From 844d8e4c7f9a3eeb681493f12c55de0392510fe3 Mon Sep 17 00:00:00 2001 From: Kurt Borja Date: Wed, 28 May 2025 07:47:22 -0300 Subject: platform/x86: alienware-wmi-wmax: Add appropriate labels to fans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add known fan type IDs and match them to an appropriate label in awcc_hwmon_read_string(). Additionally, add the AWCC_TEMP_SENSOR_FRONT type, which was inferred from it's related fan type in supported systems. Signed-off-by: Kurt Borja Reviewed-by: Armin Wolf Link: https://lore.kernel.org/r/20250528-awcc-labels-v1-1-6aa39d8e4c3d@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/dell/alienware-wmi-wmax.c | 100 ++++++++++++++++--------- 1 file changed, 63 insertions(+), 37 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/dell/alienware-wmi-wmax.c b/drivers/platform/x86/dell/alienware-wmi-wmax.c index c42f9228b0b2..b25eb3225d8e 100644 --- a/drivers/platform/x86/dell/alienware-wmi-wmax.c +++ b/drivers/platform/x86/dell/alienware-wmi-wmax.c @@ -273,9 +273,29 @@ enum AWCC_SPECIAL_THERMAL_CODES { enum AWCC_TEMP_SENSOR_TYPES { AWCC_TEMP_SENSOR_CPU = 0x01, + AWCC_TEMP_SENSOR_FRONT = 0x03, AWCC_TEMP_SENSOR_GPU = 0x06, }; +enum AWCC_FAN_TYPES { + AWCC_FAN_CPU_1 = 0x32, + AWCC_FAN_GPU_1 = 0x33, + AWCC_FAN_PCI = 0x34, + AWCC_FAN_MID = 0x35, + AWCC_FAN_TOP_1 = 0x36, + AWCC_FAN_SIDE = 0x37, + AWCC_FAN_U2_1 = 0x38, + AWCC_FAN_U2_2 = 0x39, + AWCC_FAN_FRONT_1 = 0x3A, + AWCC_FAN_CPU_2 = 0x3B, + AWCC_FAN_GPU_2 = 0x3C, + AWCC_FAN_TOP_2 = 0x3D, + AWCC_FAN_TOP_3 = 0x3E, + AWCC_FAN_FRONT_2 = 0x3F, + AWCC_FAN_BOTTOM_1 = 0x40, + AWCC_FAN_BOTTOM_2 = 0x41, +}; + enum awcc_thermal_profile { AWCC_PROFILE_USTT_BALANCED, AWCC_PROFILE_USTT_BALANCED_PERFORMANCE, @@ -314,7 +334,6 @@ struct wmax_u32_args { struct awcc_fan_data { unsigned long auto_channels_temp; - const char *label; u32 min_rpm; u32 max_rpm; u8 suspend_cache; @@ -896,6 +915,9 @@ static int awcc_hwmon_read_string(struct device *dev, enum hwmon_sensor_types ty case AWCC_TEMP_SENSOR_CPU: *str = "CPU"; break; + case AWCC_TEMP_SENSOR_FRONT: + *str = "Front"; + break; case AWCC_TEMP_SENSOR_GPU: *str = "GPU"; break; @@ -906,7 +928,46 @@ static int awcc_hwmon_read_string(struct device *dev, enum hwmon_sensor_types ty break; case hwmon_fan: - *str = priv->fan_data[channel]->label; + switch (priv->fan_data[channel]->id) { + case AWCC_FAN_CPU_1: + case AWCC_FAN_CPU_2: + *str = "CPU Fan"; + break; + case AWCC_FAN_GPU_1: + case AWCC_FAN_GPU_2: + *str = "GPU Fan"; + break; + case AWCC_FAN_PCI: + *str = "PCI Fan"; + break; + case AWCC_FAN_MID: + *str = "Mid Fan"; + break; + case AWCC_FAN_TOP_1: + case AWCC_FAN_TOP_2: + case AWCC_FAN_TOP_3: + *str = "Top Fan"; + break; + case AWCC_FAN_SIDE: + *str = "Side Fan"; + break; + case AWCC_FAN_U2_1: + case AWCC_FAN_U2_2: + *str = "U.2 Fan"; + break; + case AWCC_FAN_FRONT_1: + case AWCC_FAN_FRONT_2: + *str = "Front Fan"; + break; + case AWCC_FAN_BOTTOM_1: + case AWCC_FAN_BOTTOM_2: + *str = "Bottom Fan"; + break; + default: + *str = "Unknown Fan"; + break; + } + break; default: return -EOPNOTSUPP; @@ -1051,40 +1112,6 @@ static int awcc_hwmon_temps_init(struct wmi_device *wdev) return 0; } -static char *awcc_get_fan_label(unsigned long *fan_temps) -{ - unsigned int temp_count = bitmap_weight(fan_temps, AWCC_ID_BITMAP_SIZE); - char *label; - u8 temp_id; - - switch (temp_count) { - case 0: - label = "Independent Fan"; - break; - case 1: - temp_id = find_first_bit(fan_temps, AWCC_ID_BITMAP_SIZE); - - switch (temp_id) { - case AWCC_TEMP_SENSOR_CPU: - label = "Processor Fan"; - break; - case AWCC_TEMP_SENSOR_GPU: - label = "Video Fan"; - break; - default: - label = "Unknown Fan"; - break; - } - - break; - default: - label = "Shared Fan"; - break; - } - - return label; -} - static int awcc_hwmon_fans_init(struct wmi_device *wdev) { struct awcc_priv *priv = dev_get_drvdata(&wdev->dev); @@ -1138,7 +1165,6 @@ static int awcc_hwmon_fans_init(struct wmi_device *wdev) fan_data->id = id; fan_data->min_rpm = min_rpm; fan_data->max_rpm = max_rpm; - fan_data->label = awcc_get_fan_label(fan_temps); bitmap_gather(gather, fan_temps, priv->temp_sensors, AWCC_ID_BITMAP_SIZE); bitmap_copy(&fan_data->auto_channels_temp, gather, BITS_PER_LONG); priv->fan_data[i] = fan_data; -- cgit v1.2.3 From e7c1a9e8d33ceb44ef088de7a9112a1db94d13a4 Mon Sep 17 00:00:00 2001 From: Suma Hegde Date: Tue, 3 Jun 2025 05:58:07 +0000 Subject: platform/x86/amd/hsmp: Use IS_ENABLED() instead of IS_REACHABLE() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IS_REACHABLE() was required when CONFIG_HWMON was set to m and HSMP to y. However, commit 69157b00b526 ("platform/x86/amd/hsmp: fix building with CONFIG_HWMON=m") added a HWMON dependency for HSMP in Kconfig. With this change, using IS_ENABLED() is sufficient. Add the missing header file as well. Signed-off-by: Suma Hegde Link: https://lore.kernel.org/r/20250603055807.2503028-1-suma.hegde@amd.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/amd/hsmp/hsmp.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/amd/hsmp/hsmp.h b/drivers/platform/x86/amd/hsmp/hsmp.h index 36b5ceea9ac0..0509a442eaae 100644 --- a/drivers/platform/x86/amd/hsmp/hsmp.h +++ b/drivers/platform/x86/amd/hsmp/hsmp.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ int hsmp_misc_register(struct device *dev); int hsmp_get_tbl_dram_base(u16 sock_ind); ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size); struct hsmp_plat_device *get_hsmp_pdev(void); -#if IS_REACHABLE(CONFIG_HWMON) +#if IS_ENABLED(CONFIG_HWMON) int hsmp_create_sensor(struct device *dev, u16 sock_ind); #else static inline int hsmp_create_sensor(struct device *dev, u16 sock_ind) { return 0; } -- cgit v1.2.3 From 3dd1e9c2a279cbc9c6a7f1e7961df08c76ecb464 Mon Sep 17 00:00:00 2001 From: "Dr. David Alan Gilbert" Date: Sun, 8 Jun 2025 02:25:10 +0100 Subject: platform/x86: intel_telemetry: Remove unused telemetry_*_events() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The functions: - telemetry_add_events() - telemetry_update_events() - telemetry_reset_events() - telemetry_get_eventconfig() were all added by the commit 378f956e3f93 ("platform/x86: Add Intel Telemetry Core Driver") in 2016 but have remained unused. They're each a tiny wrapper that is the only caller through a similarly named function pointer, and for each function pointer there's a 'def' empty implementation and a plt implementation. Remove all of those components for each function. Signed-off-by: Dr. David Alan Gilbert Link: https://lore.kernel.org/r/20250608012512.377134-2-linux@treblig.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- arch/x86/include/asm/intel_telemetry.h | 24 ------ drivers/platform/x86/intel/telemetry/core.c | 101 ----------------------- drivers/platform/x86/intel/telemetry/pltdrv.c | 114 -------------------------- 3 files changed, 239 deletions(-) (limited to 'drivers') diff --git a/arch/x86/include/asm/intel_telemetry.h b/arch/x86/include/asm/intel_telemetry.h index 43b7657febca..3d7e0b922341 100644 --- a/arch/x86/include/asm/intel_telemetry.h +++ b/arch/x86/include/asm/intel_telemetry.h @@ -62,13 +62,6 @@ struct telemetry_core_ops { int (*get_sampling_period)(u8 *pss_min_period, u8 *pss_max_period, u8 *ioss_min_period, u8 *ioss_max_period); - int (*get_eventconfig)(struct telemetry_evtconfig *pss_evtconfig, - struct telemetry_evtconfig *ioss_evtconfig, - int pss_len, int ioss_len); - - int (*update_events)(struct telemetry_evtconfig pss_evtconfig, - struct telemetry_evtconfig ioss_evtconfig); - int (*set_sampling_period)(u8 pss_period, u8 ioss_period); int (*get_trace_verbosity)(enum telemetry_unit telem_unit, @@ -84,11 +77,6 @@ struct telemetry_core_ops { int (*read_eventlog)(enum telemetry_unit telem_unit, struct telemetry_evtlog *evtlog, int len, int log_all_evts); - - int (*add_events)(u8 num_pss_evts, u8 num_ioss_evts, - u32 *pss_evtmap, u32 *ioss_evtmap); - - int (*reset_events)(void); }; int telemetry_set_pltdata(const struct telemetry_core_ops *ops, @@ -101,18 +89,6 @@ struct telemetry_plt_config *telemetry_get_pltdata(void); int telemetry_get_evtname(enum telemetry_unit telem_unit, const char **name, int len); -int telemetry_update_events(struct telemetry_evtconfig pss_evtconfig, - struct telemetry_evtconfig ioss_evtconfig); - -int telemetry_add_events(u8 num_pss_evts, u8 num_ioss_evts, - u32 *pss_evtmap, u32 *ioss_evtmap); - -int telemetry_reset_events(void); - -int telemetry_get_eventconfig(struct telemetry_evtconfig *pss_config, - struct telemetry_evtconfig *ioss_config, - int pss_len, int ioss_len); - int telemetry_read_events(enum telemetry_unit telem_unit, struct telemetry_evtlog *evtlog, int len); diff --git a/drivers/platform/x86/intel/telemetry/core.c b/drivers/platform/x86/intel/telemetry/core.c index e4be40f73eeb..229e59c64af7 100644 --- a/drivers/platform/x86/intel/telemetry/core.c +++ b/drivers/platform/x86/intel/telemetry/core.c @@ -21,12 +21,6 @@ struct telemetry_core_config { static struct telemetry_core_config telm_core_conf; -static int telemetry_def_update_events(struct telemetry_evtconfig pss_evtconfig, - struct telemetry_evtconfig ioss_evtconfig) -{ - return 0; -} - static int telemetry_def_set_sampling_period(u8 pss_period, u8 ioss_period) { return 0; @@ -40,14 +34,6 @@ static int telemetry_def_get_sampling_period(u8 *pss_min_period, return 0; } -static int telemetry_def_get_eventconfig( - struct telemetry_evtconfig *pss_evtconfig, - struct telemetry_evtconfig *ioss_evtconfig, - int pss_len, int ioss_len) -{ - return 0; -} - static int telemetry_def_get_trace_verbosity(enum telemetry_unit telem_unit, u32 *verbosity) { @@ -75,51 +61,15 @@ static int telemetry_def_read_eventlog(enum telemetry_unit telem_unit, return 0; } -static int telemetry_def_add_events(u8 num_pss_evts, u8 num_ioss_evts, - u32 *pss_evtmap, u32 *ioss_evtmap) -{ - return 0; -} - -static int telemetry_def_reset_events(void) -{ - return 0; -} - static const struct telemetry_core_ops telm_defpltops = { .set_sampling_period = telemetry_def_set_sampling_period, .get_sampling_period = telemetry_def_get_sampling_period, .get_trace_verbosity = telemetry_def_get_trace_verbosity, .set_trace_verbosity = telemetry_def_set_trace_verbosity, .raw_read_eventlog = telemetry_def_raw_read_eventlog, - .get_eventconfig = telemetry_def_get_eventconfig, .read_eventlog = telemetry_def_read_eventlog, - .update_events = telemetry_def_update_events, - .reset_events = telemetry_def_reset_events, - .add_events = telemetry_def_add_events, }; -/** - * telemetry_update_events() - Update telemetry Configuration - * @pss_evtconfig: PSS related config. No change if num_evts = 0. - * @ioss_evtconfig: IOSS related config. No change if num_evts = 0. - * - * This API updates the IOSS & PSS Telemetry configuration. Old config - * is overwritten. Call telemetry_reset_events when logging is over - * All sample period values should be in the form of: - * bits[6:3] -> value; bits [0:2]-> Exponent; Period = (Value *16^Exponent) - * - * Return: 0 success, < 0 for failure - */ -int telemetry_update_events(struct telemetry_evtconfig pss_evtconfig, - struct telemetry_evtconfig ioss_evtconfig) -{ - return telm_core_conf.telem_ops->update_events(pss_evtconfig, - ioss_evtconfig); -} -EXPORT_SYMBOL_GPL(telemetry_update_events); - - /** * telemetry_set_sampling_period() - Sets the IOSS & PSS sampling period * @pss_period: placeholder for PSS Period to be set. @@ -162,57 +112,6 @@ int telemetry_get_sampling_period(u8 *pss_min_period, u8 *pss_max_period, EXPORT_SYMBOL_GPL(telemetry_get_sampling_period); -/** - * telemetry_reset_events() - Restore the IOSS & PSS configuration to default - * - * Return: 0 success, < 0 for failure - */ -int telemetry_reset_events(void) -{ - return telm_core_conf.telem_ops->reset_events(); -} -EXPORT_SYMBOL_GPL(telemetry_reset_events); - -/** - * telemetry_get_eventconfig() - Returns the pss and ioss events enabled - * @pss_evtconfig: Pointer to PSS related configuration. - * @ioss_evtconfig: Pointer to IOSS related configuration. - * @pss_len: Number of u32 elements allocated for pss_evtconfig array - * @ioss_len: Number of u32 elements allocated for ioss_evtconfig array - * - * Return: 0 success, < 0 for failure - */ -int telemetry_get_eventconfig(struct telemetry_evtconfig *pss_evtconfig, - struct telemetry_evtconfig *ioss_evtconfig, - int pss_len, int ioss_len) -{ - return telm_core_conf.telem_ops->get_eventconfig(pss_evtconfig, - ioss_evtconfig, - pss_len, ioss_len); -} -EXPORT_SYMBOL_GPL(telemetry_get_eventconfig); - -/** - * telemetry_add_events() - Add IOSS & PSS configuration to existing settings. - * @num_pss_evts: Number of PSS Events (<29) in pss_evtmap. Can be 0. - * @num_ioss_evts: Number of IOSS Events (<29) in ioss_evtmap. Can be 0. - * @pss_evtmap: Array of PSS Event-IDs to Enable - * @ioss_evtmap: Array of PSS Event-IDs to Enable - * - * Events are appended to Old Configuration. In case of total events > 28, it - * returns error. Call telemetry_reset_events to reset after eventlog done - * - * Return: 0 success, < 0 for failure - */ -int telemetry_add_events(u8 num_pss_evts, u8 num_ioss_evts, - u32 *pss_evtmap, u32 *ioss_evtmap) -{ - return telm_core_conf.telem_ops->add_events(num_pss_evts, - num_ioss_evts, pss_evtmap, - ioss_evtmap); -} -EXPORT_SYMBOL_GPL(telemetry_add_events); - /** * telemetry_read_events() - Fetches samples as specified by evtlog.telem_evt_id * @telem_unit: Specify whether IOSS or PSS Read diff --git a/drivers/platform/x86/intel/telemetry/pltdrv.c b/drivers/platform/x86/intel/telemetry/pltdrv.c index 9a499efa1e4d..60d3783de7ef 100644 --- a/drivers/platform/x86/intel/telemetry/pltdrv.c +++ b/drivers/platform/x86/intel/telemetry/pltdrv.c @@ -639,32 +639,6 @@ static int telemetry_setup(struct platform_device *pdev) return 0; } -static int telemetry_plt_update_events(struct telemetry_evtconfig pss_evtconfig, - struct telemetry_evtconfig ioss_evtconfig) -{ - int ret; - - if ((pss_evtconfig.num_evts > 0) && - (TELEM_SAMPLE_PERIOD_INVALID(pss_evtconfig.period))) { - pr_err("PSS Sampling Period Out of Range\n"); - return -EINVAL; - } - - if ((ioss_evtconfig.num_evts > 0) && - (TELEM_SAMPLE_PERIOD_INVALID(ioss_evtconfig.period))) { - pr_err("IOSS Sampling Period Out of Range\n"); - return -EINVAL; - } - - ret = telemetry_setup_evtconfig(pss_evtconfig, ioss_evtconfig, - TELEM_UPDATE); - if (ret) - pr_err("TELEMETRY Config Failed\n"); - - return ret; -} - - static int telemetry_plt_set_sampling_period(u8 pss_period, u8 ioss_period) { u32 telem_ctrl = 0; @@ -780,90 +754,6 @@ static int telemetry_plt_get_sampling_period(u8 *pss_min_period, } -static int telemetry_plt_reset_events(void) -{ - struct telemetry_evtconfig pss_evtconfig, ioss_evtconfig; - int ret; - - pss_evtconfig.evtmap = NULL; - pss_evtconfig.num_evts = TELEM_MAX_OS_ALLOCATED_EVENTS; - pss_evtconfig.period = TELEM_SAMPLING_DEFAULT_PERIOD; - - ioss_evtconfig.evtmap = NULL; - ioss_evtconfig.num_evts = TELEM_MAX_OS_ALLOCATED_EVENTS; - ioss_evtconfig.period = TELEM_SAMPLING_DEFAULT_PERIOD; - - ret = telemetry_setup_evtconfig(pss_evtconfig, ioss_evtconfig, - TELEM_RESET); - if (ret) - pr_err("TELEMETRY Reset Failed\n"); - - return ret; -} - - -static int telemetry_plt_get_eventconfig(struct telemetry_evtconfig *pss_config, - struct telemetry_evtconfig *ioss_config, - int pss_len, int ioss_len) -{ - u32 *pss_evtmap, *ioss_evtmap; - u32 index; - - pss_evtmap = pss_config->evtmap; - ioss_evtmap = ioss_config->evtmap; - - mutex_lock(&(telm_conf->telem_lock)); - pss_config->num_evts = telm_conf->pss_config.ssram_evts_used; - ioss_config->num_evts = telm_conf->ioss_config.ssram_evts_used; - - pss_config->period = telm_conf->pss_config.curr_period; - ioss_config->period = telm_conf->ioss_config.curr_period; - - if ((pss_len < telm_conf->pss_config.ssram_evts_used) || - (ioss_len < telm_conf->ioss_config.ssram_evts_used)) { - mutex_unlock(&(telm_conf->telem_lock)); - return -EINVAL; - } - - for (index = 0; index < telm_conf->pss_config.ssram_evts_used; - index++) { - pss_evtmap[index] = - telm_conf->pss_config.telem_evts[index].evt_id; - } - - for (index = 0; index < telm_conf->ioss_config.ssram_evts_used; - index++) { - ioss_evtmap[index] = - telm_conf->ioss_config.telem_evts[index].evt_id; - } - - mutex_unlock(&(telm_conf->telem_lock)); - return 0; -} - - -static int telemetry_plt_add_events(u8 num_pss_evts, u8 num_ioss_evts, - u32 *pss_evtmap, u32 *ioss_evtmap) -{ - struct telemetry_evtconfig pss_evtconfig, ioss_evtconfig; - int ret; - - pss_evtconfig.evtmap = pss_evtmap; - pss_evtconfig.num_evts = num_pss_evts; - pss_evtconfig.period = telm_conf->pss_config.curr_period; - - ioss_evtconfig.evtmap = ioss_evtmap; - ioss_evtconfig.num_evts = num_ioss_evts; - ioss_evtconfig.period = telm_conf->ioss_config.curr_period; - - ret = telemetry_setup_evtconfig(pss_evtconfig, ioss_evtconfig, - TELEM_ADD); - if (ret) - pr_err("TELEMETRY ADD Failed\n"); - - return ret; -} - static int telem_evtlog_read(enum telemetry_unit telem_unit, struct telem_ssram_region *ssram_region, u8 len) { @@ -1096,11 +986,7 @@ static const struct telemetry_core_ops telm_pltops = { .set_sampling_period = telemetry_plt_set_sampling_period, .get_sampling_period = telemetry_plt_get_sampling_period, .raw_read_eventlog = telemetry_plt_raw_read_eventlog, - .get_eventconfig = telemetry_plt_get_eventconfig, - .update_events = telemetry_plt_update_events, .read_eventlog = telemetry_plt_read_eventlog, - .reset_events = telemetry_plt_reset_events, - .add_events = telemetry_plt_add_events, }; static int telemetry_pltdrv_probe(struct platform_device *pdev) -- cgit v1.2.3 From b35b9fb28c85ccee43c34768c5e2d9a5c510e660 Mon Sep 17 00:00:00 2001 From: "Dr. David Alan Gilbert" Date: Sun, 8 Jun 2025 02:25:11 +0100 Subject: platform/x86: intel_telemetry: Remove unused telemetry_[gs]et_sampling_period() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The functions: - telemetry_get_sampling_period() - telemetry_set_sampling_period() were both added by the commit 378f956e3f93 ("platform/x86: Add Intel Telemetry Core Driver") in 2016 but have remained unused. They're each a tiny wrapper that is the only caller through a similarly named function pointer, and for each function pointer there's a 'def' empty implementation and a plt implementation. Remove all of those components for each function. Signed-off-by: Dr. David Alan Gilbert Link: https://lore.kernel.org/r/20250608012512.377134-3-linux@treblig.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- arch/x86/include/asm/intel_telemetry.h | 10 --- drivers/platform/x86/intel/telemetry/core.c | 57 ------------- drivers/platform/x86/intel/telemetry/pltdrv.c | 117 -------------------------- 3 files changed, 184 deletions(-) (limited to 'drivers') diff --git a/arch/x86/include/asm/intel_telemetry.h b/arch/x86/include/asm/intel_telemetry.h index 3d7e0b922341..e7fb005ac8d8 100644 --- a/arch/x86/include/asm/intel_telemetry.h +++ b/arch/x86/include/asm/intel_telemetry.h @@ -59,11 +59,6 @@ struct telemetry_plt_config { }; struct telemetry_core_ops { - int (*get_sampling_period)(u8 *pss_min_period, u8 *pss_max_period, - u8 *ioss_min_period, u8 *ioss_max_period); - - int (*set_sampling_period)(u8 pss_period, u8 ioss_period); - int (*get_trace_verbosity)(enum telemetry_unit telem_unit, u32 *verbosity); @@ -101,11 +96,6 @@ int telemetry_read_eventlog(enum telemetry_unit telem_unit, int telemetry_raw_read_eventlog(enum telemetry_unit telem_unit, struct telemetry_evtlog *evtlog, int len); -int telemetry_get_sampling_period(u8 *pss_min_period, u8 *pss_max_period, - u8 *ioss_min_period, u8 *ioss_max_period); - -int telemetry_set_sampling_period(u8 pss_period, u8 ioss_period); - int telemetry_set_trace_verbosity(enum telemetry_unit telem_unit, u32 verbosity); diff --git a/drivers/platform/x86/intel/telemetry/core.c b/drivers/platform/x86/intel/telemetry/core.c index 229e59c64af7..fe9e8580a8f5 100644 --- a/drivers/platform/x86/intel/telemetry/core.c +++ b/drivers/platform/x86/intel/telemetry/core.c @@ -21,19 +21,6 @@ struct telemetry_core_config { static struct telemetry_core_config telm_core_conf; -static int telemetry_def_set_sampling_period(u8 pss_period, u8 ioss_period) -{ - return 0; -} - -static int telemetry_def_get_sampling_period(u8 *pss_min_period, - u8 *pss_max_period, - u8 *ioss_min_period, - u8 *ioss_max_period) -{ - return 0; -} - static int telemetry_def_get_trace_verbosity(enum telemetry_unit telem_unit, u32 *verbosity) { @@ -62,56 +49,12 @@ static int telemetry_def_read_eventlog(enum telemetry_unit telem_unit, } static const struct telemetry_core_ops telm_defpltops = { - .set_sampling_period = telemetry_def_set_sampling_period, - .get_sampling_period = telemetry_def_get_sampling_period, .get_trace_verbosity = telemetry_def_get_trace_verbosity, .set_trace_verbosity = telemetry_def_set_trace_verbosity, .raw_read_eventlog = telemetry_def_raw_read_eventlog, .read_eventlog = telemetry_def_read_eventlog, }; -/** - * telemetry_set_sampling_period() - Sets the IOSS & PSS sampling period - * @pss_period: placeholder for PSS Period to be set. - * Set to 0 if not required to be updated - * @ioss_period: placeholder for IOSS Period to be set - * Set to 0 if not required to be updated - * - * All values should be in the form of: - * bits[6:3] -> value; bits [0:2]-> Exponent; Period = (Value *16^Exponent) - * - * Return: 0 success, < 0 for failure - */ -int telemetry_set_sampling_period(u8 pss_period, u8 ioss_period) -{ - return telm_core_conf.telem_ops->set_sampling_period(pss_period, - ioss_period); -} -EXPORT_SYMBOL_GPL(telemetry_set_sampling_period); - -/** - * telemetry_get_sampling_period() - Get IOSS & PSS min & max sampling period - * @pss_min_period: placeholder for PSS Min Period supported - * @pss_max_period: placeholder for PSS Max Period supported - * @ioss_min_period: placeholder for IOSS Min Period supported - * @ioss_max_period: placeholder for IOSS Max Period supported - * - * All values should be in the form of: - * bits[6:3] -> value; bits [0:2]-> Exponent; Period = (Value *16^Exponent) - * - * Return: 0 success, < 0 for failure - */ -int telemetry_get_sampling_period(u8 *pss_min_period, u8 *pss_max_period, - u8 *ioss_min_period, u8 *ioss_max_period) -{ - return telm_core_conf.telem_ops->get_sampling_period(pss_min_period, - pss_max_period, - ioss_min_period, - ioss_max_period); -} -EXPORT_SYMBOL_GPL(telemetry_get_sampling_period); - - /** * telemetry_read_events() - Fetches samples as specified by evtlog.telem_evt_id * @telem_unit: Specify whether IOSS or PSS Read diff --git a/drivers/platform/x86/intel/telemetry/pltdrv.c b/drivers/platform/x86/intel/telemetry/pltdrv.c index 60d3783de7ef..f23c170a55dc 100644 --- a/drivers/platform/x86/intel/telemetry/pltdrv.c +++ b/drivers/platform/x86/intel/telemetry/pltdrv.c @@ -639,121 +639,6 @@ static int telemetry_setup(struct platform_device *pdev) return 0; } -static int telemetry_plt_set_sampling_period(u8 pss_period, u8 ioss_period) -{ - u32 telem_ctrl = 0; - int ret = 0; - - mutex_lock(&(telm_conf->telem_lock)); - if (ioss_period) { - struct intel_scu_ipc_dev *scu = telm_conf->scu; - - if (TELEM_SAMPLE_PERIOD_INVALID(ioss_period)) { - pr_err("IOSS Sampling Period Out of Range\n"); - ret = -EINVAL; - goto out; - } - - /* Get telemetry EVENT CTL */ - ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, - IOSS_TELEM_EVENT_CTL_READ, NULL, 0, - &telem_ctrl, sizeof(telem_ctrl)); - if (ret) { - pr_err("IOSS TELEM_CTRL Read Failed\n"); - goto out; - } - - /* Disable Telemetry */ - TELEM_DISABLE(telem_ctrl); - - ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, - IOSS_TELEM_EVENT_CTL_WRITE, - &telem_ctrl, sizeof(telem_ctrl), - NULL, 0); - if (ret) { - pr_err("IOSS TELEM_CTRL Event Disable Write Failed\n"); - goto out; - } - - /* Enable Periodic Telemetry Events and enable SRAM trace */ - TELEM_CLEAR_SAMPLE_PERIOD(telem_ctrl); - TELEM_ENABLE_SRAM_EVT_TRACE(telem_ctrl); - TELEM_ENABLE_PERIODIC(telem_ctrl); - telem_ctrl |= ioss_period; - - ret = intel_scu_ipc_dev_command(scu, IOSS_TELEM, - IOSS_TELEM_EVENT_CTL_WRITE, - &telem_ctrl, sizeof(telem_ctrl), - NULL, 0); - if (ret) { - pr_err("IOSS TELEM_CTRL Event Enable Write Failed\n"); - goto out; - } - telm_conf->ioss_config.curr_period = ioss_period; - } - - if (pss_period) { - if (TELEM_SAMPLE_PERIOD_INVALID(pss_period)) { - pr_err("PSS Sampling Period Out of Range\n"); - ret = -EINVAL; - goto out; - } - - /* Get telemetry EVENT CTL */ - ret = intel_punit_ipc_command( - IPC_PUNIT_BIOS_READ_TELE_EVENT_CTRL, - 0, 0, NULL, &telem_ctrl); - if (ret) { - pr_err("PSS TELEM_CTRL Read Failed\n"); - goto out; - } - - /* Disable Telemetry */ - TELEM_DISABLE(telem_ctrl); - ret = intel_punit_ipc_command( - IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, - 0, 0, &telem_ctrl, NULL); - if (ret) { - pr_err("PSS TELEM_CTRL Event Disable Write Failed\n"); - goto out; - } - - /* Enable Periodic Telemetry Events and enable SRAM trace */ - TELEM_CLEAR_SAMPLE_PERIOD(telem_ctrl); - TELEM_ENABLE_SRAM_EVT_TRACE(telem_ctrl); - TELEM_ENABLE_PERIODIC(telem_ctrl); - telem_ctrl |= pss_period; - - ret = intel_punit_ipc_command( - IPC_PUNIT_BIOS_WRITE_TELE_EVENT_CTRL, - 0, 0, &telem_ctrl, NULL); - if (ret) { - pr_err("PSS TELEM_CTRL Event Enable Write Failed\n"); - goto out; - } - telm_conf->pss_config.curr_period = pss_period; - } - -out: - mutex_unlock(&(telm_conf->telem_lock)); - return ret; -} - - -static int telemetry_plt_get_sampling_period(u8 *pss_min_period, - u8 *pss_max_period, - u8 *ioss_min_period, - u8 *ioss_max_period) -{ - *pss_min_period = telm_conf->pss_config.min_period; - *pss_max_period = telm_conf->pss_config.max_period; - *ioss_min_period = telm_conf->ioss_config.min_period; - *ioss_max_period = telm_conf->ioss_config.max_period; - - return 0; -} - - static int telem_evtlog_read(enum telemetry_unit telem_unit, struct telem_ssram_region *ssram_region, u8 len) { @@ -983,8 +868,6 @@ out: static const struct telemetry_core_ops telm_pltops = { .get_trace_verbosity = telemetry_plt_get_trace_verbosity, .set_trace_verbosity = telemetry_plt_set_trace_verbosity, - .set_sampling_period = telemetry_plt_set_sampling_period, - .get_sampling_period = telemetry_plt_get_sampling_period, .raw_read_eventlog = telemetry_plt_raw_read_eventlog, .read_eventlog = telemetry_plt_read_eventlog, }; -- cgit v1.2.3 From 097cd6d6c90cc90410ab03b287166838ea71d41e Mon Sep 17 00:00:00 2001 From: "Dr. David Alan Gilbert" Date: Sun, 8 Jun 2025 02:25:12 +0100 Subject: platform/x86: intel_telemetry: Remove unused telemetry_raw_read_events() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit telemetry_raw_read_events() was added by the commit 378f956e3f93 ("platform/x86: Add Intel Telemetry Core Driver") in 2016 but has remained unused. Remove it. Signed-off-by: Dr. David Alan Gilbert Link: https://lore.kernel.org/r/20250608012512.377134-4-linux@treblig.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- arch/x86/include/asm/intel_telemetry.h | 3 --- drivers/platform/x86/intel/telemetry/core.c | 19 ------------------- 2 files changed, 22 deletions(-) (limited to 'drivers') diff --git a/arch/x86/include/asm/intel_telemetry.h b/arch/x86/include/asm/intel_telemetry.h index e7fb005ac8d8..944637a4e6de 100644 --- a/arch/x86/include/asm/intel_telemetry.h +++ b/arch/x86/include/asm/intel_telemetry.h @@ -87,9 +87,6 @@ int telemetry_get_evtname(enum telemetry_unit telem_unit, int telemetry_read_events(enum telemetry_unit telem_unit, struct telemetry_evtlog *evtlog, int len); -int telemetry_raw_read_events(enum telemetry_unit telem_unit, - struct telemetry_evtlog *evtlog, int len); - int telemetry_read_eventlog(enum telemetry_unit telem_unit, struct telemetry_evtlog *evtlog, int len); diff --git a/drivers/platform/x86/intel/telemetry/core.c b/drivers/platform/x86/intel/telemetry/core.c index fe9e8580a8f5..f312864b8d07 100644 --- a/drivers/platform/x86/intel/telemetry/core.c +++ b/drivers/platform/x86/intel/telemetry/core.c @@ -72,25 +72,6 @@ int telemetry_read_events(enum telemetry_unit telem_unit, } EXPORT_SYMBOL_GPL(telemetry_read_events); -/** - * telemetry_raw_read_events() - Fetch samples specified by evtlog.telem_evt_id - * @telem_unit: Specify whether IOSS or PSS Read - * @evtlog: Array of telemetry_evtlog structs to fill data - * evtlog.telem_evt_id specifies the ids to read - * @len: Length of array of evtlog - * - * The caller must take care of locking in this case. - * - * Return: number of eventlogs read for success, < 0 for failure - */ -int telemetry_raw_read_events(enum telemetry_unit telem_unit, - struct telemetry_evtlog *evtlog, int len) -{ - return telm_core_conf.telem_ops->raw_read_eventlog(telem_unit, evtlog, - len, 0); -} -EXPORT_SYMBOL_GPL(telemetry_raw_read_events); - /** * telemetry_read_eventlog() - Fetch the Telemetry log from PSS or IOSS * @telem_unit: Specify whether IOSS or PSS Read -- cgit v1.2.3 From fe6859aa646b9463379985165f602a44cf25c8ad Mon Sep 17 00:00:00 2001 From: Mark Pearson Date: Tue, 10 Jun 2025 15:28:24 -0400 Subject: platform/x86: thinklmi: improved DMI handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix issues reported by kernel test robot. - Require DMI for think-lmi. - Check return from getting serial string Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202506062319.F0IpDxF6-lkp@intel.com/ Signed-off-by: Mark Pearson Reviewed-by: Andy Shevchenko Link: https://lore.kernel.org/r/20250610192830.1731454-1-mpearson-lenovo@squebb.ca Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/think-lmi.c | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index e5cbd58a99f3..9f39547d98f6 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -659,6 +659,7 @@ config THINKPAD_ACPI_HOTKEY_POLL 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 diff --git a/drivers/platform/x86/think-lmi.c b/drivers/platform/x86/think-lmi.c index 00b1e7c79a3d..02ede1ec99e9 100644 --- a/drivers/platform/x86/think-lmi.c +++ b/drivers/platform/x86/think-lmi.c @@ -772,6 +772,7 @@ static ssize_t certificate_store(struct kobject *kobj, struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); enum cert_install_mode install_mode = TLMI_CERT_INSTALL; char *auth_str, *new_cert; + const char *serial; char *signature; char *guid; int ret; @@ -789,9 +790,10 @@ static ssize_t certificate_store(struct kobject *kobj, return -EACCES; /* Format: 'serial#, signature' */ - auth_str = cert_command(setting, - dmi_get_system_info(DMI_PRODUCT_SERIAL), - setting->signature); + serial = dmi_get_system_info(DMI_PRODUCT_SERIAL); + if (!serial) + return -ENODEV; + auth_str = cert_command(setting, serial, setting->signature); if (!auth_str) return -ENOMEM; -- cgit v1.2.3 From 651b57dd40871d4d0d61fb291e7f26e2b8bd69b1 Mon Sep 17 00:00:00 2001 From: Mark Pearson Date: Tue, 10 Jun 2025 15:28:25 -0400 Subject: platform/x86: Move Lenovo files into lenovo subdir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create lenovo subdirectory for holding Lenovo specific drivers. Signed-off-by: Mark Pearson Reviewed-by: Andy Shevchenko Link: https://lore.kernel.org/r/20250610192830.1731454-2-mpearson-lenovo@squebb.ca [ij: put depends on DMI back, fix trailing empty lines.] Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- MAINTAINERS | 16 +- drivers/platform/x86/Kconfig | 230 +- drivers/platform/x86/Makefile | 13 +- drivers/platform/x86/ideapad-laptop.c | 2355 ---- drivers/platform/x86/ideapad-laptop.h | 22 - drivers/platform/x86/lenovo-wmi-camera.c | 146 - drivers/platform/x86/lenovo-wmi-hotkey-utilities.c | 212 - drivers/platform/x86/lenovo-ymc.c | 165 - .../x86/lenovo-yoga-tab2-pro-1380-fastcharger.c | 339 - drivers/platform/x86/lenovo-yogabook.c | 573 - drivers/platform/x86/lenovo/Kconfig | 233 + drivers/platform/x86/lenovo/Makefile | 23 + drivers/platform/x86/lenovo/ideapad-laptop.c | 2355 ++++ drivers/platform/x86/lenovo/ideapad-laptop.h | 22 + drivers/platform/x86/lenovo/think-lmi.c | 1821 +++ drivers/platform/x86/lenovo/think-lmi.h | 126 + drivers/platform/x86/lenovo/thinkpad_acpi.c | 12096 +++++++++++++++++++ drivers/platform/x86/lenovo/wmi-camera.c | 146 + drivers/platform/x86/lenovo/wmi-hotkey-utilities.c | 212 + drivers/platform/x86/lenovo/ymc.c | 165 + .../x86/lenovo/yoga-tab2-pro-1380-fastcharger.c | 339 + drivers/platform/x86/lenovo/yogabook.c | 573 + drivers/platform/x86/think-lmi.c | 1821 --- drivers/platform/x86/think-lmi.h | 126 - drivers/platform/x86/thinkpad_acpi.c | 12096 ------------------- 25 files changed, 18127 insertions(+), 18098 deletions(-) delete mode 100644 drivers/platform/x86/ideapad-laptop.c delete mode 100644 drivers/platform/x86/ideapad-laptop.h delete mode 100644 drivers/platform/x86/lenovo-wmi-camera.c delete mode 100644 drivers/platform/x86/lenovo-wmi-hotkey-utilities.c delete mode 100644 drivers/platform/x86/lenovo-ymc.c delete mode 100644 drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c delete mode 100644 drivers/platform/x86/lenovo-yogabook.c create mode 100644 drivers/platform/x86/lenovo/Kconfig create mode 100644 drivers/platform/x86/lenovo/Makefile create mode 100644 drivers/platform/x86/lenovo/ideapad-laptop.c create mode 100644 drivers/platform/x86/lenovo/ideapad-laptop.h create mode 100644 drivers/platform/x86/lenovo/think-lmi.c create mode 100644 drivers/platform/x86/lenovo/think-lmi.h create mode 100644 drivers/platform/x86/lenovo/thinkpad_acpi.c create mode 100644 drivers/platform/x86/lenovo/wmi-camera.c create mode 100644 drivers/platform/x86/lenovo/wmi-hotkey-utilities.c create mode 100644 drivers/platform/x86/lenovo/ymc.c create mode 100644 drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c create mode 100644 drivers/platform/x86/lenovo/yogabook.c delete mode 100644 drivers/platform/x86/think-lmi.c delete mode 100644 drivers/platform/x86/think-lmi.h delete mode 100644 drivers/platform/x86/thinkpad_acpi.c (limited to 'drivers') diff --git a/MAINTAINERS b/MAINTAINERS index a92290fffa16..c14614613377 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11612,7 +11612,7 @@ M: Ike Panhc L: platform-driver-x86@vger.kernel.org S: Maintained W: http://launchpad.net/ideapad-laptop -F: drivers/platform/x86/ideapad-laptop.c +F: drivers/platform/x86/lenovo/ideapad-laptop.c IDEAPAD LAPTOP SLIDEBAR DRIVER M: Andrey Moiseev @@ -13671,11 +13671,17 @@ S: Maintained W: http://legousb.sourceforge.net/ F: drivers/usb/misc/legousbtower.c +LENOVO drivers +M: Mark Pearson +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: drivers/platform/x86/lenovo/* + LENOVO WMI HOTKEY UTILITIES DRIVER M: Jackie Dong L: platform-driver-x86@vger.kernel.org S: Maintained -F: drivers/platform/x86/lenovo-wmi-hotkey-utilities.c +F: drivers/platform/x86/lenovo/wmi-hotkey-utilities.c LETSKETCH HID TABLET DRIVER M: Hans de Goede @@ -24666,14 +24672,14 @@ S: Maintained W: http://ibm-acpi.sourceforge.net W: http://thinkwiki.org/wiki/Ibm-acpi T: git git://repo.or.cz/linux-2.6/linux-acpi-2.6/ibm-acpi-2.6.git -F: drivers/platform/x86/thinkpad_acpi.c +F: drivers/platform/x86/lenovo/thinkpad_acpi.c THINKPAD LMI DRIVER -M: Mark Pearson +M: Mark Pearson L: platform-driver-x86@vger.kernel.org S: Maintained F: Documentation/ABI/testing/sysfs-class-firmware-attributes -F: drivers/platform/x86/think-lmi.? +F: drivers/platform/x86/lenovo/think-lmi.? THP7312 ISP DRIVER M: Laurent Pinchart diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 9f39547d98f6..43055df44827 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -120,32 +120,6 @@ config GIGABYTE_WMI To compile this driver as a module, choose M here: the module will be called gigabyte-wmi. -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 ACERHDF tristate "Acer Aspire One temperature and fan driver" depends on ACPI_EC && THERMAL @@ -459,43 +433,6 @@ config IBM_RTL state = 0 (BIOS SMIs on) state = 1 (BIOS SMIs off) -config IDEAPAD_LAPTOP - tristate "Lenovo IdeaPad Laptop Extras" - depends on ACPI - 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_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 SENSORS_HDAPS tristate "Thinkpad Hard Drive Active Protection System (hdaps)" depends on INPUT @@ -514,161 +451,8 @@ config SENSORS_HDAPS Say Y here if you have an applicable laptop and want to experience the awesome power of hdaps. -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 - and - . - - 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. - source "drivers/platform/x86/intel/Kconfig" +source "drivers/platform/x86/lenovo/Kconfig" config ACPI_QUICKSTART tristate "ACPI Quickstart button driver" @@ -1079,18 +863,6 @@ config INSPUR_PLATFORM_PROFILE To compile this driver as a module, choose M here: the module will be called inspur-platform-profile. -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 DASHARO_ACPI tristate "Dasharo ACPI Platform Driver" depends on ACPI diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index abbc2644ff6d..0530a224bebd 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -58,17 +58,12 @@ obj-$(CONFIG_X86_PLATFORM_DRIVERS_HP) += hp/ # Hewlett Packard Enterprise obj-$(CONFIG_UV_SYSFS) += uv_sysfs.o -# IBM Thinkpad and Lenovo +# IBM Thinkpad (before 2005) obj-$(CONFIG_IBM_RTL) += ibm_rtl.o -obj-$(CONFIG_IDEAPAD_LAPTOP) += ideapad-laptop.o -obj-$(CONFIG_LENOVO_WMI_HOTKEY_UTILITIES) += lenovo-wmi-hotkey-utilities.o -obj-$(CONFIG_LENOVO_YMC) += lenovo-ymc.o obj-$(CONFIG_SENSORS_HDAPS) += hdaps.o -obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o -obj-$(CONFIG_THINKPAD_LMI) += think-lmi.o -obj-$(CONFIG_YOGABOOK) += lenovo-yogabook.o -obj-$(CONFIG_YT2_1380) += lenovo-yoga-tab2-pro-1380-fastcharger.o -obj-$(CONFIG_LENOVO_WMI_CAMERA) += lenovo-wmi-camera.o + +# Lenovo +obj-y += lenovo/ # Intel obj-y += intel/ diff --git a/drivers/platform/x86/ideapad-laptop.c b/drivers/platform/x86/ideapad-laptop.c deleted file mode 100644 index ede483573fe0..000000000000 --- a/drivers/platform/x86/ideapad-laptop.c +++ /dev/null @@ -1,2355 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * ideapad-laptop.c - Lenovo IdeaPad ACPI Extras - * - * Copyright © 2010 Intel Corporation - * Copyright © 2010 David Woodhouse - */ - -#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "ideapad-laptop.h" - -#include - -#include - -#define IDEAPAD_RFKILL_DEV_NUM 3 - -enum { - CFG_CAP_BT_BIT = 16, - CFG_CAP_3G_BIT = 17, - CFG_CAP_WIFI_BIT = 18, - CFG_CAP_CAM_BIT = 19, - - /* - * These are OnScreenDisplay support bits that can be useful to determine - * whether a hotkey exists/should show OSD. But they aren't particularly - * meaningful since they were introduced later, i.e. 2010 IdeaPads - * don't have these, but they still have had OSD for hotkeys. - */ - CFG_OSD_NUMLK_BIT = 27, - CFG_OSD_CAPSLK_BIT = 28, - CFG_OSD_MICMUTE_BIT = 29, - CFG_OSD_TOUCHPAD_BIT = 30, - CFG_OSD_CAM_BIT = 31, -}; - -enum { - GBMD_CONSERVATION_STATE_BIT = 5, -}; - -enum { - SBMC_CONSERVATION_ON = 3, - SBMC_CONSERVATION_OFF = 5, -}; - -enum { - HALS_KBD_BL_SUPPORT_BIT = 4, - HALS_KBD_BL_STATE_BIT = 5, - HALS_USB_CHARGING_SUPPORT_BIT = 6, - HALS_USB_CHARGING_STATE_BIT = 7, - HALS_FNLOCK_SUPPORT_BIT = 9, - HALS_FNLOCK_STATE_BIT = 10, - HALS_HOTKEYS_PRIMARY_BIT = 11, -}; - -enum { - SALS_KBD_BL_ON = 0x8, - SALS_KBD_BL_OFF = 0x9, - SALS_USB_CHARGING_ON = 0xa, - SALS_USB_CHARGING_OFF = 0xb, - SALS_FNLOCK_ON = 0xe, - SALS_FNLOCK_OFF = 0xf, -}; - -enum { - VPCCMD_R_VPC1 = 0x10, - VPCCMD_R_BL_MAX, - VPCCMD_R_BL, - VPCCMD_W_BL, - VPCCMD_R_WIFI, - VPCCMD_W_WIFI, - VPCCMD_R_BT, - VPCCMD_W_BT, - VPCCMD_R_BL_POWER, - VPCCMD_R_NOVO, - VPCCMD_R_VPC2, - VPCCMD_R_TOUCHPAD, - VPCCMD_W_TOUCHPAD, - VPCCMD_R_CAMERA, - VPCCMD_W_CAMERA, - VPCCMD_R_3G, - VPCCMD_W_3G, - VPCCMD_R_ODD, /* 0x21 */ - VPCCMD_W_FAN, - VPCCMD_R_RF, - VPCCMD_W_RF, - VPCCMD_W_YMC = 0x2A, - VPCCMD_R_FAN = 0x2B, - VPCCMD_R_SPECIAL_BUTTONS = 0x31, - VPCCMD_W_BL_POWER = 0x33, -}; - -/* - * These correspond to the number of supported states - 1 - * Future keyboard types may need a new system, if there's a collision - * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state - * so it effectively has 3 states, but needs to handle 4 - */ -enum { - KBD_BL_STANDARD = 1, - KBD_BL_TRISTATE = 2, - KBD_BL_TRISTATE_AUTO = 3, -}; - -#define KBD_BL_QUERY_TYPE 0x1 -#define KBD_BL_TRISTATE_TYPE 0x5 -#define KBD_BL_TRISTATE_AUTO_TYPE 0x7 - -#define KBD_BL_COMMAND_GET 0x2 -#define KBD_BL_COMMAND_SET 0x3 -#define KBD_BL_COMMAND_TYPE GENMASK(7, 4) - -#define KBD_BL_GET_BRIGHTNESS GENMASK(15, 1) -#define KBD_BL_SET_BRIGHTNESS GENMASK(19, 16) - -#define KBD_BL_KBLC_CHANGED_EVENT 12 - -struct ideapad_dytc_priv { - enum platform_profile_option current_profile; - struct device *ppdev; /* platform profile device */ - struct mutex mutex; /* protects the DYTC interface */ - struct ideapad_private *priv; -}; - -struct ideapad_rfk_priv { - int dev; - struct ideapad_private *priv; -}; - -struct ideapad_private { - struct acpi_device *adev; - struct mutex vpc_mutex; /* protects the VPC calls */ - struct rfkill *rfk[IDEAPAD_RFKILL_DEV_NUM]; - struct ideapad_rfk_priv rfk_priv[IDEAPAD_RFKILL_DEV_NUM]; - struct platform_device *platform_device; - struct input_dev *inputdev; - struct backlight_device *blightdev; - struct ideapad_dytc_priv *dytc; - struct dentry *debug; - unsigned long cfg; - unsigned long r_touchpad_val; - struct { - bool conservation_mode : 1; - bool dytc : 1; - bool fan_mode : 1; - bool fn_lock : 1; - bool set_fn_lock_led : 1; - bool hw_rfkill_switch : 1; - bool kbd_bl : 1; - bool touchpad_ctrl_via_ec : 1; - bool ctrl_ps2_aux_port : 1; - bool usb_charging : 1; - bool ymc_ec_trigger : 1; - } features; - struct { - bool initialized; - int type; - struct led_classdev led; - unsigned int last_brightness; - } kbd_bl; - struct { - bool initialized; - struct led_classdev led; - unsigned int last_brightness; - } fn_lock; -}; - -static bool no_bt_rfkill; -module_param(no_bt_rfkill, bool, 0444); -MODULE_PARM_DESC(no_bt_rfkill, "No rfkill for bluetooth."); - -static bool allow_v4_dytc; -module_param(allow_v4_dytc, bool, 0444); -MODULE_PARM_DESC(allow_v4_dytc, - "Enable DYTC version 4 platform-profile support. " - "If you need this please report this to: platform-driver-x86@vger.kernel.org"); - -static bool hw_rfkill_switch; -module_param(hw_rfkill_switch, bool, 0444); -MODULE_PARM_DESC(hw_rfkill_switch, - "Enable rfkill support for laptops with a hw on/off wifi switch/slider. " - "If you need this please report this to: platform-driver-x86@vger.kernel.org"); - -static bool set_fn_lock_led; -module_param(set_fn_lock_led, bool, 0444); -MODULE_PARM_DESC(set_fn_lock_led, - "Enable driver based updates of the fn-lock LED on fn-lock changes. " - "If you need this please report this to: platform-driver-x86@vger.kernel.org"); - -static bool ctrl_ps2_aux_port; -module_param(ctrl_ps2_aux_port, bool, 0444); -MODULE_PARM_DESC(ctrl_ps2_aux_port, - "Enable driver based PS/2 aux port en-/dis-abling on touchpad on/off toggle. " - "If you need this please report this to: platform-driver-x86@vger.kernel.org"); - -static bool touchpad_ctrl_via_ec; -module_param(touchpad_ctrl_via_ec, bool, 0444); -MODULE_PARM_DESC(touchpad_ctrl_via_ec, - "Enable registering a 'touchpad' sysfs-attribute which can be used to manually " - "tell the EC to enable/disable the touchpad. This may not work on all models."); - -static bool ymc_ec_trigger __read_mostly; -module_param(ymc_ec_trigger, bool, 0444); -MODULE_PARM_DESC(ymc_ec_trigger, - "Enable EC triggering work-around to force emitting tablet mode events. " - "If you need this please report this to: platform-driver-x86@vger.kernel.org"); - -/* - * shared data - */ - -static struct ideapad_private *ideapad_shared; -static DEFINE_MUTEX(ideapad_shared_mutex); - -static int ideapad_shared_init(struct ideapad_private *priv) -{ - int ret; - - guard(mutex)(&ideapad_shared_mutex); - - if (!ideapad_shared) { - ideapad_shared = priv; - ret = 0; - } else { - dev_warn(&priv->adev->dev, "found multiple platform devices\n"); - ret = -EINVAL; - } - - return ret; -} - -static void ideapad_shared_exit(struct ideapad_private *priv) -{ - guard(mutex)(&ideapad_shared_mutex); - - if (ideapad_shared == priv) - ideapad_shared = NULL; -} - -/* - * ACPI Helpers - */ -#define IDEAPAD_EC_TIMEOUT 200 /* in ms */ - -static int eval_int(acpi_handle handle, const char *name, unsigned long *res) -{ - unsigned long long result; - acpi_status status; - - status = acpi_evaluate_integer(handle, (char *)name, NULL, &result); - if (ACPI_FAILURE(status)) - return -EIO; - - *res = result; - - return 0; -} - -static int eval_int_with_arg(acpi_handle handle, const char *name, unsigned long arg, - unsigned long *res) -{ - struct acpi_object_list params; - unsigned long long result; - union acpi_object in_obj; - acpi_status status; - - params.count = 1; - params.pointer = &in_obj; - in_obj.type = ACPI_TYPE_INTEGER; - in_obj.integer.value = arg; - - status = acpi_evaluate_integer(handle, (char *)name, ¶ms, &result); - if (ACPI_FAILURE(status)) - return -EIO; - - if (res) - *res = result; - - return 0; -} - -static int exec_simple_method(acpi_handle handle, const char *name, unsigned long arg) -{ - acpi_status status = acpi_execute_simple_method(handle, (char *)name, arg); - - return ACPI_FAILURE(status) ? -EIO : 0; -} - -static int eval_gbmd(acpi_handle handle, unsigned long *res) -{ - return eval_int(handle, "GBMD", res); -} - -static int exec_sbmc(acpi_handle handle, unsigned long arg) -{ - return exec_simple_method(handle, "SBMC", arg); -} - -static int eval_hals(acpi_handle handle, unsigned long *res) -{ - return eval_int(handle, "HALS", res); -} - -static int exec_sals(acpi_handle handle, unsigned long arg) -{ - return exec_simple_method(handle, "SALS", arg); -} - -static int exec_kblc(acpi_handle handle, unsigned long arg) -{ - return exec_simple_method(handle, "KBLC", arg); -} - -static int eval_kblc(acpi_handle handle, unsigned long cmd, unsigned long *res) -{ - return eval_int_with_arg(handle, "KBLC", cmd, res); -} - -static int eval_dytc(acpi_handle handle, unsigned long cmd, unsigned long *res) -{ - return eval_int_with_arg(handle, "DYTC", cmd, res); -} - -static int eval_vpcr(acpi_handle handle, unsigned long cmd, unsigned long *res) -{ - return eval_int_with_arg(handle, "VPCR", cmd, res); -} - -static int eval_vpcw(acpi_handle handle, unsigned long cmd, unsigned long data) -{ - struct acpi_object_list params; - union acpi_object in_obj[2]; - acpi_status status; - - params.count = 2; - params.pointer = in_obj; - in_obj[0].type = ACPI_TYPE_INTEGER; - in_obj[0].integer.value = cmd; - in_obj[1].type = ACPI_TYPE_INTEGER; - in_obj[1].integer.value = data; - - status = acpi_evaluate_object(handle, "VPCW", ¶ms, NULL); - if (ACPI_FAILURE(status)) - return -EIO; - - return 0; -} - -static int read_ec_data(acpi_handle handle, unsigned long cmd, unsigned long *data) -{ - unsigned long end_jiffies, val; - int err; - - err = eval_vpcw(handle, 1, cmd); - if (err) - return err; - - end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; - - while (time_before(jiffies, end_jiffies)) { - schedule(); - - err = eval_vpcr(handle, 1, &val); - if (err) - return err; - - if (val == 0) - return eval_vpcr(handle, 0, data); - } - - acpi_handle_err(handle, "timeout in %s\n", __func__); - - return -ETIMEDOUT; -} - -static int write_ec_cmd(acpi_handle handle, unsigned long cmd, unsigned long data) -{ - unsigned long end_jiffies, val; - int err; - - err = eval_vpcw(handle, 0, data); - if (err) - return err; - - err = eval_vpcw(handle, 1, cmd); - if (err) - return err; - - end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; - - while (time_before(jiffies, end_jiffies)) { - schedule(); - - err = eval_vpcr(handle, 1, &val); - if (err) - return err; - - if (val == 0) - return 0; - } - - acpi_handle_err(handle, "timeout in %s\n", __func__); - - return -ETIMEDOUT; -} - -/* - * debugfs - */ -static int debugfs_status_show(struct seq_file *s, void *data) -{ - struct ideapad_private *priv = s->private; - unsigned long value; - - guard(mutex)(&priv->vpc_mutex); - - if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &value)) - seq_printf(s, "Backlight max: %lu\n", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL, &value)) - seq_printf(s, "Backlight now: %lu\n", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &value)) - seq_printf(s, "BL power value: %s (%lu)\n", value ? "on" : "off", value); - - seq_puts(s, "=====================\n"); - - if (!read_ec_data(priv->adev->handle, VPCCMD_R_RF, &value)) - seq_printf(s, "Radio status: %s (%lu)\n", value ? "on" : "off", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_WIFI, &value)) - seq_printf(s, "Wifi status: %s (%lu)\n", value ? "on" : "off", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_BT, &value)) - seq_printf(s, "BT status: %s (%lu)\n", value ? "on" : "off", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_3G, &value)) - seq_printf(s, "3G status: %s (%lu)\n", value ? "on" : "off", value); - - seq_puts(s, "=====================\n"); - - if (!read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value)) - seq_printf(s, "Touchpad status: %s (%lu)\n", value ? "on" : "off", value); - if (!read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &value)) - seq_printf(s, "Camera status: %s (%lu)\n", value ? "on" : "off", value); - - seq_puts(s, "=====================\n"); - - if (!eval_gbmd(priv->adev->handle, &value)) - seq_printf(s, "GBMD: %#010lx\n", value); - if (!eval_hals(priv->adev->handle, &value)) - seq_printf(s, "HALS: %#010lx\n", value); - - return 0; -} -DEFINE_SHOW_ATTRIBUTE(debugfs_status); - -static int debugfs_cfg_show(struct seq_file *s, void *data) -{ - struct ideapad_private *priv = s->private; - - seq_printf(s, "_CFG: %#010lx\n\n", priv->cfg); - - seq_puts(s, "Capabilities:"); - if (test_bit(CFG_CAP_BT_BIT, &priv->cfg)) - seq_puts(s, " bluetooth"); - if (test_bit(CFG_CAP_3G_BIT, &priv->cfg)) - seq_puts(s, " 3G"); - if (test_bit(CFG_CAP_WIFI_BIT, &priv->cfg)) - seq_puts(s, " wifi"); - if (test_bit(CFG_CAP_CAM_BIT, &priv->cfg)) - seq_puts(s, " camera"); - seq_puts(s, "\n"); - - seq_puts(s, "OSD support:"); - if (test_bit(CFG_OSD_NUMLK_BIT, &priv->cfg)) - seq_puts(s, " num-lock"); - if (test_bit(CFG_OSD_CAPSLK_BIT, &priv->cfg)) - seq_puts(s, " caps-lock"); - if (test_bit(CFG_OSD_MICMUTE_BIT, &priv->cfg)) - seq_puts(s, " mic-mute"); - if (test_bit(CFG_OSD_TOUCHPAD_BIT, &priv->cfg)) - seq_puts(s, " touchpad"); - if (test_bit(CFG_OSD_CAM_BIT, &priv->cfg)) - seq_puts(s, " camera"); - seq_puts(s, "\n"); - - seq_puts(s, "Graphics: "); - switch (priv->cfg & 0x700) { - case 0x100: - seq_puts(s, "Intel"); - break; - case 0x200: - seq_puts(s, "ATI"); - break; - case 0x300: - seq_puts(s, "Nvidia"); - break; - case 0x400: - seq_puts(s, "Intel and ATI"); - break; - case 0x500: - seq_puts(s, "Intel and Nvidia"); - break; - } - seq_puts(s, "\n"); - - return 0; -} -DEFINE_SHOW_ATTRIBUTE(debugfs_cfg); - -static void ideapad_debugfs_init(struct ideapad_private *priv) -{ - struct dentry *dir; - - dir = debugfs_create_dir("ideapad", NULL); - priv->debug = dir; - - debugfs_create_file("cfg", 0444, dir, priv, &debugfs_cfg_fops); - debugfs_create_file("status", 0444, dir, priv, &debugfs_status_fops); -} - -static void ideapad_debugfs_exit(struct ideapad_private *priv) -{ - debugfs_remove_recursive(priv->debug); - priv->debug = NULL; -} - -/* - * sysfs - */ -static ssize_t camera_power_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned long result = 0; - int err; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &result); - if (err) - return err; - } - - return sysfs_emit(buf, "%d\n", !!result); -} - -static ssize_t camera_power_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - bool state; - int err; - - err = kstrtobool(buf, &state); - if (err) - return err; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = write_ec_cmd(priv->adev->handle, VPCCMD_W_CAMERA, state); - if (err) - return err; - } - - return count; -} - -static DEVICE_ATTR_RW(camera_power); - -static ssize_t conservation_mode_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned long result; - int err; - - err = eval_gbmd(priv->adev->handle, &result); - if (err) - return err; - - return sysfs_emit(buf, "%d\n", !!test_bit(GBMD_CONSERVATION_STATE_BIT, &result)); -} - -static ssize_t conservation_mode_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - bool state; - int err; - - err = kstrtobool(buf, &state); - if (err) - return err; - - err = exec_sbmc(priv->adev->handle, state ? SBMC_CONSERVATION_ON : SBMC_CONSERVATION_OFF); - if (err) - return err; - - return count; -} - -static DEVICE_ATTR_RW(conservation_mode); - -static ssize_t fan_mode_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned long result = 0; - int err; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = read_ec_data(priv->adev->handle, VPCCMD_R_FAN, &result); - if (err) - return err; - } - - return sysfs_emit(buf, "%lu\n", result); -} - -static ssize_t fan_mode_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned int state; - int err; - - err = kstrtouint(buf, 0, &state); - if (err) - return err; - - if (state > 4 || state == 3) - return -EINVAL; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = write_ec_cmd(priv->adev->handle, VPCCMD_W_FAN, state); - if (err) - return err; - } - - return count; -} - -static DEVICE_ATTR_RW(fan_mode); - -static int ideapad_fn_lock_get(struct ideapad_private *priv) -{ - unsigned long hals; - int err; - - err = eval_hals(priv->adev->handle, &hals); - if (err) - return err; - - return !!test_bit(HALS_FNLOCK_STATE_BIT, &hals); -} - -static int ideapad_fn_lock_set(struct ideapad_private *priv, bool state) -{ - return exec_sals(priv->adev->handle, - state ? SALS_FNLOCK_ON : SALS_FNLOCK_OFF); -} - -static void ideapad_fn_lock_led_notify(struct ideapad_private *priv, int brightness) -{ - if (!priv->fn_lock.initialized) - return; - - if (brightness == priv->fn_lock.last_brightness) - return; - - priv->fn_lock.last_brightness = brightness; - - led_classdev_notify_brightness_hw_changed(&priv->fn_lock.led, brightness); -} - -static ssize_t fn_lock_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - int brightness; - - brightness = ideapad_fn_lock_get(priv); - if (brightness < 0) - return brightness; - - return sysfs_emit(buf, "%d\n", brightness); -} - -static ssize_t fn_lock_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - bool state; - int err; - - err = kstrtobool(buf, &state); - if (err) - return err; - - err = ideapad_fn_lock_set(priv, state); - if (err) - return err; - - ideapad_fn_lock_led_notify(priv, state); - - return count; -} - -static DEVICE_ATTR_RW(fn_lock); - -static ssize_t touchpad_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned long result = 0; - int err; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &result); - if (err) - return err; - } - - priv->r_touchpad_val = result; - - return sysfs_emit(buf, "%d\n", !!result); -} - -static ssize_t touchpad_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - bool state; - int err; - - err = kstrtobool(buf, &state); - if (err) - return err; - - scoped_guard(mutex, &priv->vpc_mutex) { - err = write_ec_cmd(priv->adev->handle, VPCCMD_W_TOUCHPAD, state); - if (err) - return err; - } - - priv->r_touchpad_val = state; - - return count; -} - -static DEVICE_ATTR_RW(touchpad); - -static ssize_t usb_charging_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - unsigned long hals; - int err; - - err = eval_hals(priv->adev->handle, &hals); - if (err) - return err; - - return sysfs_emit(buf, "%d\n", !!test_bit(HALS_USB_CHARGING_STATE_BIT, &hals)); -} - -static ssize_t usb_charging_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - bool state; - int err; - - err = kstrtobool(buf, &state); - if (err) - return err; - - err = exec_sals(priv->adev->handle, state ? SALS_USB_CHARGING_ON : SALS_USB_CHARGING_OFF); - if (err) - return err; - - return count; -} - -static DEVICE_ATTR_RW(usb_charging); - -static struct attribute *ideapad_attributes[] = { - &dev_attr_camera_power.attr, - &dev_attr_conservation_mode.attr, - &dev_attr_fan_mode.attr, - &dev_attr_fn_lock.attr, - &dev_attr_touchpad.attr, - &dev_attr_usb_charging.attr, - NULL -}; - -static umode_t ideapad_is_visible(struct kobject *kobj, - struct attribute *attr, - int idx) -{ - struct device *dev = kobj_to_dev(kobj); - struct ideapad_private *priv = dev_get_drvdata(dev); - bool supported = true; - - if (attr == &dev_attr_camera_power.attr) - supported = test_bit(CFG_CAP_CAM_BIT, &priv->cfg); - else if (attr == &dev_attr_conservation_mode.attr) - supported = priv->features.conservation_mode; - else if (attr == &dev_attr_fan_mode.attr) - supported = priv->features.fan_mode; - else if (attr == &dev_attr_fn_lock.attr) - supported = priv->features.fn_lock; - else if (attr == &dev_attr_touchpad.attr) - supported = priv->features.touchpad_ctrl_via_ec; - else if (attr == &dev_attr_usb_charging.attr) - supported = priv->features.usb_charging; - - return supported ? attr->mode : 0; -} - -static const struct attribute_group ideapad_attribute_group = { - .is_visible = ideapad_is_visible, - .attrs = ideapad_attributes -}; -__ATTRIBUTE_GROUPS(ideapad_attribute); - -/* - * DYTC Platform profile - */ -#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ -#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ -#define DYTC_CMD_GET 2 /* To get current IC function and mode */ -#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ - -#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ -#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ -#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ - -#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ -#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ - -#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ -#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ -#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ - -#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ -#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ -#define DYTC_FUNCTION_MMC 11 /* Function = 11, desk mode */ - -#define DYTC_MODE_PERFORM 2 /* High power mode aka performance */ -#define DYTC_MODE_LOW_POWER 3 /* Low power mode aka quiet */ -#define DYTC_MODE_BALANCE 0xF /* Default mode aka balanced */ - -#define DYTC_SET_COMMAND(function, mode, on) \ - (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ - (mode) << DYTC_SET_MODE_BIT | \ - (on) << DYTC_SET_VALID_BIT) - -#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 0) - -#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 1) - -static int convert_dytc_to_profile(int dytcmode, enum platform_profile_option *profile) -{ - switch (dytcmode) { - case DYTC_MODE_LOW_POWER: - *profile = PLATFORM_PROFILE_LOW_POWER; - break; - case DYTC_MODE_BALANCE: - *profile = PLATFORM_PROFILE_BALANCED; - break; - case DYTC_MODE_PERFORM: - *profile = PLATFORM_PROFILE_PERFORMANCE; - break; - default: /* Unknown mode */ - return -EINVAL; - } - - return 0; -} - -static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) -{ - switch (profile) { - case PLATFORM_PROFILE_LOW_POWER: - *perfmode = DYTC_MODE_LOW_POWER; - break; - case PLATFORM_PROFILE_BALANCED: - *perfmode = DYTC_MODE_BALANCE; - break; - case PLATFORM_PROFILE_PERFORMANCE: - *perfmode = DYTC_MODE_PERFORM; - break; - default: /* Unknown profile */ - return -EOPNOTSUPP; - } - - return 0; -} - -/* - * dytc_profile_get: Function to register with platform_profile - * handler. Returns current platform profile. - */ -static int dytc_profile_get(struct device *dev, - enum platform_profile_option *profile) -{ - struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); - - *profile = dytc->current_profile; - return 0; -} - -/* - * Helper function - check if we are in CQL mode and if we are - * - disable CQL, - * - run the command - * - enable CQL - * If not in CQL mode, just run the command - */ -static int dytc_cql_command(struct ideapad_private *priv, unsigned long cmd, - unsigned long *output) -{ - int err, cmd_err, cur_funcmode; - - /* Determine if we are in CQL mode. This alters the commands we do */ - err = eval_dytc(priv->adev->handle, DYTC_CMD_GET, output); - if (err) - return err; - - cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; - /* Check if we're OK to return immediately */ - if (cmd == DYTC_CMD_GET && cur_funcmode != DYTC_FUNCTION_CQL) - return 0; - - if (cur_funcmode == DYTC_FUNCTION_CQL) { - err = eval_dytc(priv->adev->handle, DYTC_DISABLE_CQL, NULL); - if (err) - return err; - } - - cmd_err = eval_dytc(priv->adev->handle, cmd, output); - /* Check return condition after we've restored CQL state */ - - if (cur_funcmode == DYTC_FUNCTION_CQL) { - err = eval_dytc(priv->adev->handle, DYTC_ENABLE_CQL, NULL); - if (err) - return err; - } - - return cmd_err; -} - -/* - * dytc_profile_set: Function to register with platform_profile - * handler. Sets current platform profile. - */ -static int dytc_profile_set(struct device *dev, - enum platform_profile_option profile) -{ - struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); - struct ideapad_private *priv = dytc->priv; - unsigned long output; - int err; - - scoped_guard(mutex_intr, &dytc->mutex) { - if (profile == PLATFORM_PROFILE_BALANCED) { - /* To get back to balanced mode we just issue a reset command */ - err = eval_dytc(priv->adev->handle, DYTC_CMD_RESET, NULL); - if (err) - return err; - } else { - int perfmode; - - err = convert_profile_to_dytc(profile, &perfmode); - if (err) - return err; - - /* Determine if we are in CQL mode. This alters the commands we do */ - err = dytc_cql_command(priv, - DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), - &output); - if (err) - return err; - } - - /* Success - update current profile */ - dytc->current_profile = profile; - return 0; - } - - return -EINTR; -} - -static int dytc_profile_probe(void *drvdata, unsigned long *choices) -{ - set_bit(PLATFORM_PROFILE_LOW_POWER, choices); - set_bit(PLATFORM_PROFILE_BALANCED, choices); - set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); - - return 0; -} - -static void dytc_profile_refresh(struct ideapad_private *priv) -{ - enum platform_profile_option profile; - unsigned long output; - int err, perfmode; - - scoped_guard(mutex, &priv->dytc->mutex) - err = dytc_cql_command(priv, DYTC_CMD_GET, &output); - if (err) - return; - - perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; - - if (convert_dytc_to_profile(perfmode, &profile)) - return; - - if (profile != priv->dytc->current_profile) { - priv->dytc->current_profile = profile; - platform_profile_notify(priv->dytc->ppdev); - } -} - -static const struct dmi_system_id ideapad_dytc_v4_allow_table[] = { - { - /* Ideapad 5 Pro 16ACH6 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_NAME, "82L5") - } - }, - { - /* Ideapad 5 15ITL05 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_VERSION, "IdeaPad 5 15ITL05") - } - }, - {} -}; - -static const struct platform_profile_ops dytc_profile_ops = { - .probe = dytc_profile_probe, - .profile_get = dytc_profile_get, - .profile_set = dytc_profile_set, -}; - -static int ideapad_dytc_profile_init(struct ideapad_private *priv) -{ - int err, dytc_version; - unsigned long output; - - if (!priv->features.dytc) - return -ENODEV; - - err = eval_dytc(priv->adev->handle, DYTC_CMD_QUERY, &output); - /* For all other errors we can flag the failure */ - if (err) - return err; - - /* Check DYTC is enabled and supports mode setting */ - if (!test_bit(DYTC_QUERY_ENABLE_BIT, &output)) { - dev_info(&priv->platform_device->dev, "DYTC_QUERY_ENABLE_BIT returned false\n"); - return -ENODEV; - } - - dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; - - if (dytc_version < 4) { - dev_info(&priv->platform_device->dev, "DYTC_VERSION < 4 is not supported\n"); - return -ENODEV; - } - - if (dytc_version < 5 && - !(allow_v4_dytc || dmi_check_system(ideapad_dytc_v4_allow_table))) { - dev_info(&priv->platform_device->dev, - "DYTC_VERSION 4 support may not work. Pass ideapad_laptop.allow_v4_dytc=Y on the kernel commandline to enable\n"); - return -ENODEV; - } - - priv->dytc = kzalloc(sizeof(*priv->dytc), GFP_KERNEL); - if (!priv->dytc) - return -ENOMEM; - - mutex_init(&priv->dytc->mutex); - - priv->dytc->priv = priv; - - /* Create platform_profile structure and register */ - priv->dytc->ppdev = devm_platform_profile_register(&priv->platform_device->dev, - "ideapad-laptop", priv->dytc, - &dytc_profile_ops); - if (IS_ERR(priv->dytc->ppdev)) { - err = PTR_ERR(priv->dytc->ppdev); - goto pp_reg_failed; - } - - /* Ensure initial values are correct */ - dytc_profile_refresh(priv); - - return 0; - -pp_reg_failed: - mutex_destroy(&priv->dytc->mutex); - kfree(priv->dytc); - priv->dytc = NULL; - - return err; -} - -static void ideapad_dytc_profile_exit(struct ideapad_private *priv) -{ - if (!priv->dytc) - return; - - mutex_destroy(&priv->dytc->mutex); - kfree(priv->dytc); - - priv->dytc = NULL; -} - -/* - * Rfkill - */ -struct ideapad_rfk_data { - char *name; - int cfgbit; - int opcode; - int type; -}; - -static const struct ideapad_rfk_data ideapad_rfk_data[] = { - { "ideapad_wlan", CFG_CAP_WIFI_BIT, VPCCMD_W_WIFI, RFKILL_TYPE_WLAN }, - { "ideapad_bluetooth", CFG_CAP_BT_BIT, VPCCMD_W_BT, RFKILL_TYPE_BLUETOOTH }, - { "ideapad_3g", CFG_CAP_3G_BIT, VPCCMD_W_3G, RFKILL_TYPE_WWAN }, -}; - -static int ideapad_rfk_set(void *data, bool blocked) -{ - struct ideapad_rfk_priv *priv = data; - int opcode = ideapad_rfk_data[priv->dev].opcode; - - guard(mutex)(&priv->priv->vpc_mutex); - - return write_ec_cmd(priv->priv->adev->handle, opcode, !blocked); -} - -static const struct rfkill_ops ideapad_rfk_ops = { - .set_block = ideapad_rfk_set, -}; - -static void ideapad_sync_rfk_state(struct ideapad_private *priv) -{ - unsigned long hw_blocked = 0; - int i; - - if (priv->features.hw_rfkill_switch) { - guard(mutex)(&priv->vpc_mutex); - - if (read_ec_data(priv->adev->handle, VPCCMD_R_RF, &hw_blocked)) - return; - hw_blocked = !hw_blocked; - } - - for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) - if (priv->rfk[i]) - rfkill_set_hw_state(priv->rfk[i], hw_blocked); -} - -static int ideapad_register_rfkill(struct ideapad_private *priv, int dev) -{ - unsigned long rf_enabled; - int err; - - if (no_bt_rfkill && ideapad_rfk_data[dev].type == RFKILL_TYPE_BLUETOOTH) { - /* Force to enable bluetooth when no_bt_rfkill=1 */ - write_ec_cmd(priv->adev->handle, ideapad_rfk_data[dev].opcode, 1); - return 0; - } - - priv->rfk_priv[dev].dev = dev; - priv->rfk_priv[dev].priv = priv; - - priv->rfk[dev] = rfkill_alloc(ideapad_rfk_data[dev].name, - &priv->platform_device->dev, - ideapad_rfk_data[dev].type, - &ideapad_rfk_ops, - &priv->rfk_priv[dev]); - if (!priv->rfk[dev]) - return -ENOMEM; - - err = read_ec_data(priv->adev->handle, ideapad_rfk_data[dev].opcode - 1, &rf_enabled); - if (err) - rf_enabled = 1; - - rfkill_init_sw_state(priv->rfk[dev], !rf_enabled); - - err = rfkill_register(priv->rfk[dev]); - if (err) - rfkill_destroy(priv->rfk[dev]); - - return err; -} - -static void ideapad_unregister_rfkill(struct ideapad_private *priv, int dev) -{ - if (!priv->rfk[dev]) - return; - - rfkill_unregister(priv->rfk[dev]); - rfkill_destroy(priv->rfk[dev]); -} - -/* - * input device - */ -#define IDEAPAD_WMI_KEY 0x100 - -static const struct key_entry ideapad_keymap[] = { - { KE_KEY, 6, { KEY_SWITCHVIDEOMODE } }, - { KE_KEY, 7, { KEY_CAMERA } }, - { KE_KEY, 8, { KEY_MICMUTE } }, - { KE_KEY, 11, { KEY_F16 } }, - { KE_KEY, 13, { KEY_WLAN } }, - { KE_KEY, 16, { KEY_PROG1 } }, - { KE_KEY, 17, { KEY_PROG2 } }, - { KE_KEY, 64, { KEY_PROG3 } }, - { KE_KEY, 65, { KEY_PROG4 } }, - { KE_KEY, 66, { KEY_TOUCHPAD_OFF } }, - { KE_KEY, 67, { KEY_TOUCHPAD_ON } }, - { KE_KEY, 128, { KEY_ESC } }, - - /* - * WMI keys - */ - - /* FnLock (handled by the firmware) */ - { KE_IGNORE, 0x02 | IDEAPAD_WMI_KEY }, - /* Esc (handled by the firmware) */ - { KE_IGNORE, 0x03 | IDEAPAD_WMI_KEY }, - /* Customizable Lenovo Hotkey ("star" with 'S' inside) */ - { KE_KEY, 0x01 | IDEAPAD_WMI_KEY, { KEY_FAVORITES } }, - { KE_KEY, 0x04 | IDEAPAD_WMI_KEY, { KEY_SELECTIVE_SCREENSHOT } }, - /* Lenovo Support */ - { KE_KEY, 0x07 | IDEAPAD_WMI_KEY, { KEY_HELP } }, - { KE_KEY, 0x0e | IDEAPAD_WMI_KEY, { KEY_PICKUP_PHONE } }, - { KE_KEY, 0x0f | IDEAPAD_WMI_KEY, { KEY_HANGUP_PHONE } }, - /* Refresh Rate Toggle (Fn+R) */ - { KE_KEY, 0x10 | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, - /* Dark mode toggle */ - { KE_KEY, 0x13 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, - /* Sound profile switch */ - { KE_KEY, 0x12 | IDEAPAD_WMI_KEY, { KEY_PROG2 } }, - /* Lenovo Virtual Background application */ - { KE_KEY, 0x28 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, - /* Lenovo Support */ - { KE_KEY, 0x27 | IDEAPAD_WMI_KEY, { KEY_HELP } }, - /* Refresh Rate Toggle */ - { KE_KEY, 0x0a | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, - /* Specific to some newer models */ - { KE_KEY, 0x3e | IDEAPAD_WMI_KEY, { KEY_MICMUTE } }, - { KE_KEY, 0x3f | IDEAPAD_WMI_KEY, { KEY_RFKILL } }, - /* Star- (User Assignable Key) */ - { KE_KEY, 0x44 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, - /* Eye */ - { KE_KEY, 0x45 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, - /* Performance toggle also Fn+Q, handled inside ideapad_wmi_notify() */ - { KE_KEY, 0x3d | IDEAPAD_WMI_KEY, { KEY_PROG4 } }, - /* shift + prtsc */ - { KE_KEY, 0x2d | IDEAPAD_WMI_KEY, { KEY_CUT } }, - { KE_KEY, 0x29 | IDEAPAD_WMI_KEY, { KEY_TOUCHPAD_TOGGLE } }, - { KE_KEY, 0x2a | IDEAPAD_WMI_KEY, { KEY_ROOT_MENU } }, - - { KE_END }, -}; - -static int ideapad_input_init(struct ideapad_private *priv) -{ - struct input_dev *inputdev; - int err; - - inputdev = input_allocate_device(); - if (!inputdev) - return -ENOMEM; - - inputdev->name = "Ideapad extra buttons"; - inputdev->phys = "ideapad/input0"; - inputdev->id.bustype = BUS_HOST; - inputdev->dev.parent = &priv->platform_device->dev; - - err = sparse_keymap_setup(inputdev, ideapad_keymap, NULL); - if (err) { - dev_err(&priv->platform_device->dev, - "Could not set up input device keymap: %d\n", err); - goto err_free_dev; - } - - err = input_register_device(inputdev); - if (err) { - dev_err(&priv->platform_device->dev, - "Could not register input device: %d\n", err); - goto err_free_dev; - } - - priv->inputdev = inputdev; - - return 0; - -err_free_dev: - input_free_device(inputdev); - - return err; -} - -static void ideapad_input_exit(struct ideapad_private *priv) -{ - input_unregister_device(priv->inputdev); - priv->inputdev = NULL; -} - -static void ideapad_input_report(struct ideapad_private *priv, - unsigned long scancode) -{ - sparse_keymap_report_event(priv->inputdev, scancode, 1, true); -} - -static void ideapad_input_novokey(struct ideapad_private *priv) -{ - unsigned long long_pressed; - - scoped_guard(mutex, &priv->vpc_mutex) - if (read_ec_data(priv->adev->handle, VPCCMD_R_NOVO, &long_pressed)) - return; - - if (long_pressed) - ideapad_input_report(priv, 17); - else - ideapad_input_report(priv, 16); -} - -static void ideapad_check_special_buttons(struct ideapad_private *priv) -{ - unsigned long bit, value; - - scoped_guard(mutex, &priv->vpc_mutex) - if (read_ec_data(priv->adev->handle, VPCCMD_R_SPECIAL_BUTTONS, &value)) - return; - - for_each_set_bit (bit, &value, 16) { - switch (bit) { - case 6: /* Z570 */ - case 0: /* Z580 */ - /* Thermal Management / Performance Mode button */ - if (priv->dytc) - platform_profile_cycle(); - else - ideapad_input_report(priv, 65); - break; - case 1: - /* OneKey Theater button */ - ideapad_input_report(priv, 64); - break; - default: - dev_info(&priv->platform_device->dev, - "Unknown special button: %lu\n", bit); - break; - } - } -} - -/* - * backlight - */ -static int ideapad_backlight_get_brightness(struct backlight_device *blightdev) -{ - struct ideapad_private *priv = bl_get_data(blightdev); - unsigned long now; - int err; - - guard(mutex)(&priv->vpc_mutex); - - err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); - if (err) - return err; - - return now; -} - -static int ideapad_backlight_update_status(struct backlight_device *blightdev) -{ - struct ideapad_private *priv = bl_get_data(blightdev); - int err; - - guard(mutex)(&priv->vpc_mutex); - - err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL, - blightdev->props.brightness); - if (err) - return err; - - err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL_POWER, - blightdev->props.power != BACKLIGHT_POWER_OFF); - if (err) - return err; - - return 0; -} - -static const struct backlight_ops ideapad_backlight_ops = { - .get_brightness = ideapad_backlight_get_brightness, - .update_status = ideapad_backlight_update_status, -}; - -static int ideapad_backlight_init(struct ideapad_private *priv) -{ - struct backlight_device *blightdev; - struct backlight_properties props; - unsigned long max, now, power; - int err; - - err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &max); - if (err) - return err; - - err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); - if (err) - return err; - - err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power); - if (err) - return err; - - memset(&props, 0, sizeof(props)); - - props.max_brightness = max; - props.type = BACKLIGHT_PLATFORM; - - blightdev = backlight_device_register("ideapad", - &priv->platform_device->dev, - priv, - &ideapad_backlight_ops, - &props); - if (IS_ERR(blightdev)) { - err = PTR_ERR(blightdev); - dev_err(&priv->platform_device->dev, - "Could not register backlight device: %d\n", err); - return err; - } - - priv->blightdev = blightdev; - blightdev->props.brightness = now; - blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; - - backlight_update_status(blightdev); - - return 0; -} - -static void ideapad_backlight_exit(struct ideapad_private *priv) -{ - backlight_device_unregister(priv->blightdev); - priv->blightdev = NULL; -} - -static void ideapad_backlight_notify_power(struct ideapad_private *priv) -{ - struct backlight_device *blightdev = priv->blightdev; - unsigned long power; - - if (!blightdev) - return; - - guard(mutex)(&priv->vpc_mutex); - - if (read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power)) - return; - - blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; -} - -static void ideapad_backlight_notify_brightness(struct ideapad_private *priv) -{ - unsigned long now; - - /* if we control brightness via acpi video driver */ - if (!priv->blightdev) - scoped_guard(mutex, &priv->vpc_mutex) - read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); - else - backlight_force_update(priv->blightdev, BACKLIGHT_UPDATE_HOTKEY); -} - -/* - * keyboard backlight - */ -static int ideapad_kbd_bl_check_tristate(int type) -{ - return (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO); -} - -static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv) -{ - unsigned long value; - int err; - - if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) { - err = eval_kblc(priv->adev->handle, - FIELD_PREP(KBD_BL_COMMAND_TYPE, priv->kbd_bl.type) | - KBD_BL_COMMAND_GET, - &value); - - if (err) - return err; - - /* Convert returned value to brightness level */ - value = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value); - - /* Off, low or high */ - if (value <= priv->kbd_bl.led.max_brightness) - return value; - - /* Auto, report as off */ - if (value == priv->kbd_bl.led.max_brightness + 1) - return 0; - - /* Unknown value */ - dev_warn(&priv->platform_device->dev, - "Unknown keyboard backlight value: %lu", value); - return -EINVAL; - } - - err = eval_hals(priv->adev->handle, &value); - if (err) - return err; - - return !!test_bit(HALS_KBD_BL_STATE_BIT, &value); -} - -static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev) -{ - struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); - - return ideapad_kbd_bl_brightness_get(priv); -} - -static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness) -{ - int err; - unsigned long value; - int type = priv->kbd_bl.type; - - if (ideapad_kbd_bl_check_tristate(type)) { - if (brightness > priv->kbd_bl.led.max_brightness) - return -EINVAL; - - value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) | - FIELD_PREP(KBD_BL_COMMAND_TYPE, type) | - KBD_BL_COMMAND_SET; - err = exec_kblc(priv->adev->handle, value); - } else { - err = exec_sals(priv->adev->handle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF); - } - - if (err) - return err; - - priv->kbd_bl.last_brightness = brightness; - - return 0; -} - -static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); - - return ideapad_kbd_bl_brightness_set(priv, brightness); -} - -static void ideapad_kbd_bl_notify(struct ideapad_private *priv) -{ - int brightness; - - if (!priv->kbd_bl.initialized) - return; - - brightness = ideapad_kbd_bl_brightness_get(priv); - if (brightness < 0) - return; - - if (brightness == priv->kbd_bl.last_brightness) - return; - - priv->kbd_bl.last_brightness = brightness; - - led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness); -} - -static int ideapad_kbd_bl_init(struct ideapad_private *priv) -{ - int brightness, err; - - if (!priv->features.kbd_bl) - return -ENODEV; - - if (WARN_ON(priv->kbd_bl.initialized)) - return -EEXIST; - - if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) { - priv->kbd_bl.led.max_brightness = 2; - } else { - priv->kbd_bl.led.max_brightness = 1; - } - - brightness = ideapad_kbd_bl_brightness_get(priv); - if (brightness < 0) - return brightness; - - priv->kbd_bl.last_brightness = brightness; - priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT; - priv->kbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get; - priv->kbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set; - priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED; - - err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led); - if (err) - return err; - - priv->kbd_bl.initialized = true; - - return 0; -} - -static void ideapad_kbd_bl_exit(struct ideapad_private *priv) -{ - if (!priv->kbd_bl.initialized) - return; - - priv->kbd_bl.initialized = false; - - led_classdev_unregister(&priv->kbd_bl.led); -} - -/* - * FnLock LED - */ -static enum led_brightness ideapad_fn_lock_led_cdev_get(struct led_classdev *led_cdev) -{ - struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); - - return ideapad_fn_lock_get(priv); -} - -static int ideapad_fn_lock_led_cdev_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); - - return ideapad_fn_lock_set(priv, brightness); -} - -static int ideapad_fn_lock_led_init(struct ideapad_private *priv) -{ - int brightness, err; - - if (!priv->features.fn_lock) - return -ENODEV; - - if (WARN_ON(priv->fn_lock.initialized)) - return -EEXIST; - - priv->fn_lock.led.max_brightness = 1; - - brightness = ideapad_fn_lock_get(priv); - if (brightness < 0) - return brightness; - - priv->fn_lock.last_brightness = brightness; - priv->fn_lock.led.name = "platform::" LED_FUNCTION_FNLOCK; - priv->fn_lock.led.brightness_get = ideapad_fn_lock_led_cdev_get; - priv->fn_lock.led.brightness_set_blocking = ideapad_fn_lock_led_cdev_set; - priv->fn_lock.led.flags = LED_BRIGHT_HW_CHANGED; - - err = led_classdev_register(&priv->platform_device->dev, &priv->fn_lock.led); - if (err) - return err; - - priv->fn_lock.initialized = true; - - return 0; -} - -static void ideapad_fn_lock_led_exit(struct ideapad_private *priv) -{ - if (!priv->fn_lock.initialized) - return; - - priv->fn_lock.initialized = false; - - led_classdev_unregister(&priv->fn_lock.led); -} - -/* - * module init/exit - */ -static void ideapad_sync_touchpad_state(struct ideapad_private *priv, bool send_events) -{ - unsigned long value; - unsigned char param; - int ret; - - /* Without reading from EC touchpad LED doesn't switch state */ - scoped_guard(mutex, &priv->vpc_mutex) - ret = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value); - if (ret) - return; - - /* - * Some IdeaPads don't really turn off touchpad - they only - * switch the LED state. We (de)activate KBC AUX port to turn - * touchpad off and on. We send KEY_TOUCHPAD_OFF and - * KEY_TOUCHPAD_ON to not to get out of sync with LED - */ - if (priv->features.ctrl_ps2_aux_port) - i8042_command(¶m, value ? I8042_CMD_AUX_ENABLE : I8042_CMD_AUX_DISABLE); - - /* - * On older models the EC controls the touchpad and toggles it on/off - * itself, in this case we report KEY_TOUCHPAD_ON/_OFF. Some models do - * an acpi-notify with VPC bit 5 set on resume, so this function get - * called with send_events=true on every resume. Therefor if the EC did - * not toggle, do nothing to avoid sending spurious KEY_TOUCHPAD_TOGGLE. - */ - if (send_events && value != priv->r_touchpad_val) { - ideapad_input_report(priv, value ? 67 : 66); - sysfs_notify(&priv->platform_device->dev.kobj, NULL, "touchpad"); - } - - priv->r_touchpad_val = value; -} - -static const struct dmi_system_id ymc_ec_trigger_quirk_dmi_table[] = { - { - /* Lenovo Yoga 7 14ARB7 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_NAME, "82QF"), - }, - }, - { - /* Lenovo Yoga 7 14ACN6 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_NAME, "82N7"), - }, - }, - { } -}; - -static void ideapad_laptop_trigger_ec(void) -{ - struct ideapad_private *priv; - int ret; - - guard(mutex)(&ideapad_shared_mutex); - - priv = ideapad_shared; - if (!priv) - return; - - if (!priv->features.ymc_ec_trigger) - return; - - scoped_guard(mutex, &priv->vpc_mutex) - ret = write_ec_cmd(priv->adev->handle, VPCCMD_W_YMC, 1); - if (ret) - dev_warn(&priv->platform_device->dev, "Could not write YMC: %d\n", ret); -} - -static int ideapad_laptop_nb_notify(struct notifier_block *nb, - unsigned long action, void *data) -{ - switch (action) { - case IDEAPAD_LAPTOP_YMC_EVENT: - ideapad_laptop_trigger_ec(); - break; - } - - return 0; -} - -static struct notifier_block ideapad_laptop_notifier = { - .notifier_call = ideapad_laptop_nb_notify, -}; - -static BLOCKING_NOTIFIER_HEAD(ideapad_laptop_chain_head); - -int ideapad_laptop_register_notifier(struct notifier_block *nb) -{ - return blocking_notifier_chain_register(&ideapad_laptop_chain_head, nb); -} -EXPORT_SYMBOL_NS_GPL(ideapad_laptop_register_notifier, "IDEAPAD_LAPTOP"); - -int ideapad_laptop_unregister_notifier(struct notifier_block *nb) -{ - return blocking_notifier_chain_unregister(&ideapad_laptop_chain_head, nb); -} -EXPORT_SYMBOL_NS_GPL(ideapad_laptop_unregister_notifier, "IDEAPAD_LAPTOP"); - -void ideapad_laptop_call_notifier(unsigned long action, void *data) -{ - blocking_notifier_call_chain(&ideapad_laptop_chain_head, action, data); -} -EXPORT_SYMBOL_NS_GPL(ideapad_laptop_call_notifier, "IDEAPAD_LAPTOP"); - -static void ideapad_acpi_notify(acpi_handle handle, u32 event, void *data) -{ - struct ideapad_private *priv = data; - unsigned long vpc1, vpc2, bit; - - scoped_guard(mutex, &priv->vpc_mutex) { - if (read_ec_data(handle, VPCCMD_R_VPC1, &vpc1)) - return; - - if (read_ec_data(handle, VPCCMD_R_VPC2, &vpc2)) - return; - } - - vpc1 = (vpc2 << 8) | vpc1; - - for_each_set_bit (bit, &vpc1, 16) { - switch (bit) { - case 13: - case 11: - case 8: - case 7: - case 6: - ideapad_input_report(priv, bit); - break; - case 10: - /* - * This event gets send on a Yoga 300-11IBR when the EC - * believes that the device has changed between laptop/ - * tent/stand/tablet mode. The EC relies on getting - * angle info from 2 accelerometers through a special - * windows service calling a DSM on the DUAL250E ACPI- - * device. Linux does not do this, making the laptop/ - * tent/stand/tablet mode info unreliable, so we simply - * ignore these events. - */ - break; - case 9: - ideapad_sync_rfk_state(priv); - break; - case 5: - ideapad_sync_touchpad_state(priv, true); - break; - case 4: - ideapad_backlight_notify_brightness(priv); - break; - case 3: - ideapad_input_novokey(priv); - break; - case 2: - ideapad_backlight_notify_power(priv); - break; - case KBD_BL_KBLC_CHANGED_EVENT: - case 1: - /* - * Some IdeaPads report event 1 every ~20 - * seconds while on battery power; some - * report this when changing to/from tablet - * mode; some report this when the keyboard - * backlight has changed. - */ - ideapad_kbd_bl_notify(priv); - break; - case 0: - ideapad_check_special_buttons(priv); - break; - default: - dev_info(&priv->platform_device->dev, - "Unknown event: %lu\n", bit); - } - } -} - -/* On some models we need to call exec_sals(SALS_FNLOCK_ON/OFF) to set the LED */ -static const struct dmi_system_id set_fn_lock_led_list[] = { - { - /* https://bugzilla.kernel.org/show_bug.cgi?id=212671 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion R7000P2020H"), - } - }, - { - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion 5 15ARH05"), - } - }, - {} -}; - -/* - * Some ideapads have a hardware rfkill switch, but most do not have one. - * Reading VPCCMD_R_RF always results in 0 on models without a hardware rfkill, - * switch causing ideapad_laptop to wrongly report all radios as hw-blocked. - * There used to be a long list of DMI ids for models without a hw rfkill - * switch here, but that resulted in playing whack a mole. - * More importantly wrongly reporting the wifi radio as hw-blocked, results in - * non working wifi. Whereas not reporting it hw-blocked, when it actually is - * hw-blocked results in an empty SSID list, which is a much more benign - * failure mode. - * So the default now is the much safer option of assuming there is no - * hardware rfkill switch. This default also actually matches most hardware, - * since having a hw rfkill switch is quite rare on modern hardware, so this - * also leads to a much shorter list. - */ -static const struct dmi_system_id hw_rfkill_list[] = { - {} -}; - -/* - * On some models the EC toggles the touchpad muted LED on touchpad toggle - * hotkey presses, but the EC does not actually disable the touchpad itself. - * On these models the driver needs to explicitly enable/disable the i8042 - * (PS/2) aux port. - */ -static const struct dmi_system_id ctrl_ps2_aux_port_list[] = { - { - /* Lenovo Ideapad Z570 */ - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_PRODUCT_VERSION, "Ideapad Z570"), - }, - }, - {} -}; - -static void ideapad_check_features(struct ideapad_private *priv) -{ - acpi_handle handle = priv->adev->handle; - unsigned long val; - - priv->features.set_fn_lock_led = - set_fn_lock_led || dmi_check_system(set_fn_lock_led_list); - priv->features.hw_rfkill_switch = - hw_rfkill_switch || dmi_check_system(hw_rfkill_list); - priv->features.ctrl_ps2_aux_port = - ctrl_ps2_aux_port || dmi_check_system(ctrl_ps2_aux_port_list); - priv->features.touchpad_ctrl_via_ec = touchpad_ctrl_via_ec; - priv->features.ymc_ec_trigger = - ymc_ec_trigger || dmi_check_system(ymc_ec_trigger_quirk_dmi_table); - - if (!read_ec_data(handle, VPCCMD_R_FAN, &val)) - priv->features.fan_mode = true; - - if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) - priv->features.conservation_mode = true; - - if (acpi_has_method(handle, "DYTC")) - priv->features.dytc = true; - - if (acpi_has_method(handle, "HALS") && acpi_has_method(handle, "SALS")) { - if (!eval_hals(handle, &val)) { - if (test_bit(HALS_FNLOCK_SUPPORT_BIT, &val)) - priv->features.fn_lock = true; - - if (test_bit(HALS_KBD_BL_SUPPORT_BIT, &val)) { - priv->features.kbd_bl = true; - priv->kbd_bl.type = KBD_BL_STANDARD; - } - - if (test_bit(HALS_USB_CHARGING_SUPPORT_BIT, &val)) - priv->features.usb_charging = true; - } - } - - if (acpi_has_method(handle, "KBLC")) { - if (!eval_kblc(priv->adev->handle, KBD_BL_QUERY_TYPE, &val)) { - if (val == KBD_BL_TRISTATE_TYPE) { - priv->features.kbd_bl = true; - priv->kbd_bl.type = KBD_BL_TRISTATE; - } else if (val == KBD_BL_TRISTATE_AUTO_TYPE) { - priv->features.kbd_bl = true; - priv->kbd_bl.type = KBD_BL_TRISTATE_AUTO; - } else { - dev_warn(&priv->platform_device->dev, - "Unknown keyboard type: %lu", - val); - } - } - } -} - -#if IS_ENABLED(CONFIG_ACPI_WMI) -/* - * WMI driver - */ -enum ideapad_wmi_event_type { - IDEAPAD_WMI_EVENT_ESC, - IDEAPAD_WMI_EVENT_FN_KEYS, -}; - -struct ideapad_wmi_private { - enum ideapad_wmi_event_type event; -}; - -static int ideapad_wmi_probe(struct wmi_device *wdev, const void *context) -{ - struct ideapad_wmi_private *wpriv; - - wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL); - if (!wpriv) - return -ENOMEM; - - *wpriv = *(const struct ideapad_wmi_private *)context; - - dev_set_drvdata(&wdev->dev, wpriv); - return 0; -} - -static void ideapad_wmi_notify(struct wmi_device *wdev, union acpi_object *data) -{ - struct ideapad_wmi_private *wpriv = dev_get_drvdata(&wdev->dev); - struct ideapad_private *priv; - - guard(mutex)(&ideapad_shared_mutex); - - priv = ideapad_shared; - if (!priv) - return; - - switch (wpriv->event) { - case IDEAPAD_WMI_EVENT_ESC: - ideapad_input_report(priv, 128); - break; - case IDEAPAD_WMI_EVENT_FN_KEYS: - if (priv->features.set_fn_lock_led) { - int brightness = ideapad_fn_lock_get(priv); - - if (brightness >= 0) { - ideapad_fn_lock_set(priv, brightness); - ideapad_fn_lock_led_notify(priv, brightness); - } - } - - if (data->type != ACPI_TYPE_INTEGER) { - dev_warn(&wdev->dev, - "WMI event data is not an integer\n"); - break; - } - - dev_dbg(&wdev->dev, "WMI fn-key event: 0x%llx\n", - data->integer.value); - - /* performance button triggered by 0x3d */ - if (data->integer.value == 0x3d && priv->dytc) { - platform_profile_cycle(); - break; - } - - /* 0x02 FnLock, 0x03 Esc */ - if (data->integer.value == 0x02 || data->integer.value == 0x03) - ideapad_fn_lock_led_notify(priv, data->integer.value == 0x02); - - ideapad_input_report(priv, - data->integer.value | IDEAPAD_WMI_KEY); - - break; - } -} - -static const struct ideapad_wmi_private ideapad_wmi_context_esc = { - .event = IDEAPAD_WMI_EVENT_ESC -}; - -static const struct ideapad_wmi_private ideapad_wmi_context_fn_keys = { - .event = IDEAPAD_WMI_EVENT_FN_KEYS -}; - -static const struct wmi_device_id ideapad_wmi_ids[] = { - { "26CAB2E5-5CF1-46AE-AAC3-4A12B6BA50E6", &ideapad_wmi_context_esc }, /* Yoga 3 */ - { "56322276-8493-4CE8-A783-98C991274F5E", &ideapad_wmi_context_esc }, /* Yoga 700 */ - { "8FC0DE0C-B4E4-43FD-B0F3-8871711C1294", &ideapad_wmi_context_fn_keys }, /* Legion 5 */ - {}, -}; -MODULE_DEVICE_TABLE(wmi, ideapad_wmi_ids); - -static struct wmi_driver ideapad_wmi_driver = { - .driver = { - .name = "ideapad_wmi", - }, - .id_table = ideapad_wmi_ids, - .probe = ideapad_wmi_probe, - .notify = ideapad_wmi_notify, -}; - -static int ideapad_wmi_driver_register(void) -{ - return wmi_driver_register(&ideapad_wmi_driver); -} - -static void ideapad_wmi_driver_unregister(void) -{ - return wmi_driver_unregister(&ideapad_wmi_driver); -} - -#else -static inline int ideapad_wmi_driver_register(void) { return 0; } -static inline void ideapad_wmi_driver_unregister(void) { } -#endif - -/* - * ACPI driver - */ -static int ideapad_acpi_add(struct platform_device *pdev) -{ - struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); - struct ideapad_private *priv; - acpi_status status; - unsigned long cfg; - int err, i; - - if (!adev || eval_int(adev->handle, "_CFG", &cfg)) - return -ENODEV; - - priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - dev_set_drvdata(&pdev->dev, priv); - - priv->cfg = cfg; - priv->adev = adev; - priv->platform_device = pdev; - - err = devm_mutex_init(&pdev->dev, &priv->vpc_mutex); - if (err) - return err; - - ideapad_check_features(priv); - - ideapad_debugfs_init(priv); - - err = ideapad_input_init(priv); - if (err) - goto input_failed; - - err = ideapad_kbd_bl_init(priv); - if (err) { - if (err != -ENODEV) - dev_warn(&pdev->dev, "Could not set up keyboard backlight LED: %d\n", err); - else - dev_info(&pdev->dev, "Keyboard backlight control not available\n"); - } - - err = ideapad_fn_lock_led_init(priv); - if (err) { - if (err != -ENODEV) - dev_warn(&pdev->dev, "Could not set up FnLock LED: %d\n", err); - else - dev_info(&pdev->dev, "FnLock control not available\n"); - } - - /* - * On some models without a hw-switch (the yoga 2 13 at least) - * VPCCMD_W_RF must be explicitly set to 1 for the wifi to work. - */ - if (!priv->features.hw_rfkill_switch) - write_ec_cmd(priv->adev->handle, VPCCMD_W_RF, 1); - - for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) - if (test_bit(ideapad_rfk_data[i].cfgbit, &priv->cfg)) - ideapad_register_rfkill(priv, i); - - ideapad_sync_rfk_state(priv); - ideapad_sync_touchpad_state(priv, false); - - err = ideapad_dytc_profile_init(priv); - if (err) { - if (err != -ENODEV) - dev_warn(&pdev->dev, "Could not set up DYTC interface: %d\n", err); - else - dev_info(&pdev->dev, "DYTC interface is not available\n"); - } - - if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { - err = ideapad_backlight_init(priv); - if (err && err != -ENODEV) - goto backlight_failed; - } - - status = acpi_install_notify_handler(adev->handle, - ACPI_DEVICE_NOTIFY, - ideapad_acpi_notify, priv); - if (ACPI_FAILURE(status)) { - err = -EIO; - goto notification_failed; - } - - err = ideapad_shared_init(priv); - if (err) - goto shared_init_failed; - - ideapad_laptop_register_notifier(&ideapad_laptop_notifier); - - return 0; - -shared_init_failed: - acpi_remove_notify_handler(priv->adev->handle, - ACPI_DEVICE_NOTIFY, - ideapad_acpi_notify); - -notification_failed: - ideapad_backlight_exit(priv); - -backlight_failed: - ideapad_dytc_profile_exit(priv); - - for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) - ideapad_unregister_rfkill(priv, i); - - ideapad_fn_lock_led_exit(priv); - ideapad_kbd_bl_exit(priv); - ideapad_input_exit(priv); - -input_failed: - ideapad_debugfs_exit(priv); - - return err; -} - -static void ideapad_acpi_remove(struct platform_device *pdev) -{ - struct ideapad_private *priv = dev_get_drvdata(&pdev->dev); - int i; - - ideapad_laptop_unregister_notifier(&ideapad_laptop_notifier); - - ideapad_shared_exit(priv); - - acpi_remove_notify_handler(priv->adev->handle, - ACPI_DEVICE_NOTIFY, - ideapad_acpi_notify); - - ideapad_backlight_exit(priv); - ideapad_dytc_profile_exit(priv); - - for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) - ideapad_unregister_rfkill(priv, i); - - ideapad_fn_lock_led_exit(priv); - ideapad_kbd_bl_exit(priv); - ideapad_input_exit(priv); - ideapad_debugfs_exit(priv); -} - -#ifdef CONFIG_PM_SLEEP -static int ideapad_acpi_resume(struct device *dev) -{ - struct ideapad_private *priv = dev_get_drvdata(dev); - - ideapad_sync_rfk_state(priv); - ideapad_sync_touchpad_state(priv, false); - - if (priv->dytc) - dytc_profile_refresh(priv); - - return 0; -} -#endif -static SIMPLE_DEV_PM_OPS(ideapad_pm, NULL, ideapad_acpi_resume); - -static const struct acpi_device_id ideapad_device_ids[] = { - {"VPC2004", 0}, - {"", 0}, -}; -MODULE_DEVICE_TABLE(acpi, ideapad_device_ids); - -static struct platform_driver ideapad_acpi_driver = { - .probe = ideapad_acpi_add, - .remove = ideapad_acpi_remove, - .driver = { - .name = "ideapad_acpi", - .pm = &ideapad_pm, - .acpi_match_table = ACPI_PTR(ideapad_device_ids), - .dev_groups = ideapad_attribute_groups, - }, -}; - -static int __init ideapad_laptop_init(void) -{ - int err; - - err = ideapad_wmi_driver_register(); - if (err) - return err; - - err = platform_driver_register(&ideapad_acpi_driver); - if (err) { - ideapad_wmi_driver_unregister(); - return err; - } - - return 0; -} -module_init(ideapad_laptop_init) - -static void __exit ideapad_laptop_exit(void) -{ - ideapad_wmi_driver_unregister(); - platform_driver_unregister(&ideapad_acpi_driver); -} -module_exit(ideapad_laptop_exit) - -MODULE_AUTHOR("David Woodhouse "); -MODULE_DESCRIPTION("IdeaPad ACPI Extras"); -MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/ideapad-laptop.h b/drivers/platform/x86/ideapad-laptop.h deleted file mode 100644 index 1e52f2aa0aac..000000000000 --- a/drivers/platform/x86/ideapad-laptop.h +++ /dev/null @@ -1,22 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * ideapad-laptop.h - Lenovo IdeaPad ACPI Extras - * - * Copyright © 2010 Intel Corporation - * Copyright © 2010 David Woodhouse - */ - -#ifndef _IDEAPAD_LAPTOP_H_ -#define _IDEAPAD_LAPTOP_H_ - -#include - -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_ */ diff --git a/drivers/platform/x86/lenovo-wmi-camera.c b/drivers/platform/x86/lenovo-wmi-camera.c deleted file mode 100644 index eb60fb9a5b3f..000000000000 --- a/drivers/platform/x86/lenovo-wmi-camera.c +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * Lenovo WMI Camera Button Driver - * - * Author: Ai Chao - * Copyright (C) 2024 KylinSoft Corporation. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#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, - .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 "); -MODULE_DESCRIPTION("Lenovo WMI Camera Button Driver"); -MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo-wmi-hotkey-utilities.c b/drivers/platform/x86/lenovo-wmi-hotkey-utilities.c deleted file mode 100644 index 89153afd7015..000000000000 --- a/drivers/platform/x86/lenovo-wmi-hotkey-utilities.c +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * Lenovo Super Hotkey Utility WMI extras driver for Ideapad laptop - * - * Copyright (C) 2025 Lenovo - */ - -#include -#include -#include -#include -#include -#include - -/* 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) - led_version = obj->integer.value; - else - return -EIO; - - wpriv->cdev[led_type].max_brightness = LED_ON; - wpriv->cdev[led_type].flags = LED_CORE_SUSPENDRESUME; - - switch (led_type) { - case MIC_MUTE: - if (led_version != WMI_LUD_SUPPORT_MICMUTE_LED_VER) - return -EIO; - - 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) - return -EIO; - - 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; - } - - 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 "); -MODULE_DESCRIPTION("Lenovo Super Hotkey Utility WMI extras driver"); -MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo-ymc.c b/drivers/platform/x86/lenovo-ymc.c deleted file mode 100644 index 470d53e3c9d2..000000000000 --- a/drivers/platform/x86/lenovo-ymc.c +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * lenovo-ymc.c - Lenovo Yoga Mode Control driver - * - * Copyright © 2022 Gergo Koteles - */ - -#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -#include -#include -#include -#include -#include -#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, - .probe = lenovo_ymc_probe, - .notify = lenovo_ymc_notify, -}; - -module_wmi_driver(lenovo_ymc_driver); - -MODULE_AUTHOR("Gergo Koteles "); -MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver"); -MODULE_LICENSE("GPL"); -MODULE_IMPORT_NS("IDEAPAD_LAPTOP"); diff --git a/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c b/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c deleted file mode 100644 index 25933cd018d1..000000000000 --- a/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c +++ /dev/null @@ -1,339 +0,0 @@ -// 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 - */ -#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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 = pinctrl_register_mappings(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)) { - ret = PTR_ERR(ctrl_dev); - goto out_pinctrl_unregister_mappings; - } - - serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); - put_device(ctrl_dev); - if (!serdev) { - ret = -ENOMEM; - goto out_pinctrl_unregister_mappings; - } - - ret = serdev_device_add(serdev); - if (ret) { - dev_err_probe(&pdev->dev, ret, "adding serdev\n"); - serdev_device_put(serdev); - goto out_pinctrl_unregister_mappings; - } - - /* - * 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 */ - ret = (ret == -EAGAIN) ? -EPROBE_DEFER : ret; - dev_err_probe(&pdev->dev, ret, "attaching serdev driver\n"); - goto out_serdev_device_remove; - } - - /* So that yt2_1380_fc_pdev_remove() can remove the serdev */ - platform_set_drvdata(pdev, serdev); - return 0; - -out_serdev_device_remove: - serdev_device_remove(serdev); -out_pinctrl_unregister_mappings: - pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); - return ret; -} - -static void yt2_1380_fc_pdev_remove(struct platform_device *pdev) -{ - struct serdev_device *serdev = platform_get_drvdata(pdev); - - serdev_device_remove(serdev); - pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); -} - -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 "); -MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo-yogabook.c b/drivers/platform/x86/lenovo-yogabook.c deleted file mode 100644 index 31b298dc5046..000000000000 --- a/drivers/platform/x86/lenovo-yogabook.c +++ /dev/null @@ -1,573 +0,0 @@ -// 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 - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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; - uint8_t 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 = ¶m; - - 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 == NULL) - 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), - }, - .no_notify_data = true, - .id_table = yogabook_wmi_id_table, - .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 == NULL) - 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"); diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig new file mode 100644 index 000000000000..e9cb7372a2ca --- /dev/null +++ b/drivers/platform/x86/lenovo/Kconfig @@ -0,0 +1,233 @@ +# 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 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 + and + . + + 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. diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile new file mode 100644 index 000000000000..f1aa2449293b --- /dev/null +++ b/drivers/platform/x86/lenovo/Makefile @@ -0,0 +1,23 @@ +# 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 + +# 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))) diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c new file mode 100644 index 000000000000..ede483573fe0 --- /dev/null +++ b/drivers/platform/x86/lenovo/ideapad-laptop.c @@ -0,0 +1,2355 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ideapad-laptop.c - Lenovo IdeaPad ACPI Extras + * + * Copyright © 2010 Intel Corporation + * Copyright © 2010 David Woodhouse + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ideapad-laptop.h" + +#include + +#include + +#define IDEAPAD_RFKILL_DEV_NUM 3 + +enum { + CFG_CAP_BT_BIT = 16, + CFG_CAP_3G_BIT = 17, + CFG_CAP_WIFI_BIT = 18, + CFG_CAP_CAM_BIT = 19, + + /* + * These are OnScreenDisplay support bits that can be useful to determine + * whether a hotkey exists/should show OSD. But they aren't particularly + * meaningful since they were introduced later, i.e. 2010 IdeaPads + * don't have these, but they still have had OSD for hotkeys. + */ + CFG_OSD_NUMLK_BIT = 27, + CFG_OSD_CAPSLK_BIT = 28, + CFG_OSD_MICMUTE_BIT = 29, + CFG_OSD_TOUCHPAD_BIT = 30, + CFG_OSD_CAM_BIT = 31, +}; + +enum { + GBMD_CONSERVATION_STATE_BIT = 5, +}; + +enum { + SBMC_CONSERVATION_ON = 3, + SBMC_CONSERVATION_OFF = 5, +}; + +enum { + HALS_KBD_BL_SUPPORT_BIT = 4, + HALS_KBD_BL_STATE_BIT = 5, + HALS_USB_CHARGING_SUPPORT_BIT = 6, + HALS_USB_CHARGING_STATE_BIT = 7, + HALS_FNLOCK_SUPPORT_BIT = 9, + HALS_FNLOCK_STATE_BIT = 10, + HALS_HOTKEYS_PRIMARY_BIT = 11, +}; + +enum { + SALS_KBD_BL_ON = 0x8, + SALS_KBD_BL_OFF = 0x9, + SALS_USB_CHARGING_ON = 0xa, + SALS_USB_CHARGING_OFF = 0xb, + SALS_FNLOCK_ON = 0xe, + SALS_FNLOCK_OFF = 0xf, +}; + +enum { + VPCCMD_R_VPC1 = 0x10, + VPCCMD_R_BL_MAX, + VPCCMD_R_BL, + VPCCMD_W_BL, + VPCCMD_R_WIFI, + VPCCMD_W_WIFI, + VPCCMD_R_BT, + VPCCMD_W_BT, + VPCCMD_R_BL_POWER, + VPCCMD_R_NOVO, + VPCCMD_R_VPC2, + VPCCMD_R_TOUCHPAD, + VPCCMD_W_TOUCHPAD, + VPCCMD_R_CAMERA, + VPCCMD_W_CAMERA, + VPCCMD_R_3G, + VPCCMD_W_3G, + VPCCMD_R_ODD, /* 0x21 */ + VPCCMD_W_FAN, + VPCCMD_R_RF, + VPCCMD_W_RF, + VPCCMD_W_YMC = 0x2A, + VPCCMD_R_FAN = 0x2B, + VPCCMD_R_SPECIAL_BUTTONS = 0x31, + VPCCMD_W_BL_POWER = 0x33, +}; + +/* + * These correspond to the number of supported states - 1 + * Future keyboard types may need a new system, if there's a collision + * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state + * so it effectively has 3 states, but needs to handle 4 + */ +enum { + KBD_BL_STANDARD = 1, + KBD_BL_TRISTATE = 2, + KBD_BL_TRISTATE_AUTO = 3, +}; + +#define KBD_BL_QUERY_TYPE 0x1 +#define KBD_BL_TRISTATE_TYPE 0x5 +#define KBD_BL_TRISTATE_AUTO_TYPE 0x7 + +#define KBD_BL_COMMAND_GET 0x2 +#define KBD_BL_COMMAND_SET 0x3 +#define KBD_BL_COMMAND_TYPE GENMASK(7, 4) + +#define KBD_BL_GET_BRIGHTNESS GENMASK(15, 1) +#define KBD_BL_SET_BRIGHTNESS GENMASK(19, 16) + +#define KBD_BL_KBLC_CHANGED_EVENT 12 + +struct ideapad_dytc_priv { + enum platform_profile_option current_profile; + struct device *ppdev; /* platform profile device */ + struct mutex mutex; /* protects the DYTC interface */ + struct ideapad_private *priv; +}; + +struct ideapad_rfk_priv { + int dev; + struct ideapad_private *priv; +}; + +struct ideapad_private { + struct acpi_device *adev; + struct mutex vpc_mutex; /* protects the VPC calls */ + struct rfkill *rfk[IDEAPAD_RFKILL_DEV_NUM]; + struct ideapad_rfk_priv rfk_priv[IDEAPAD_RFKILL_DEV_NUM]; + struct platform_device *platform_device; + struct input_dev *inputdev; + struct backlight_device *blightdev; + struct ideapad_dytc_priv *dytc; + struct dentry *debug; + unsigned long cfg; + unsigned long r_touchpad_val; + struct { + bool conservation_mode : 1; + bool dytc : 1; + bool fan_mode : 1; + bool fn_lock : 1; + bool set_fn_lock_led : 1; + bool hw_rfkill_switch : 1; + bool kbd_bl : 1; + bool touchpad_ctrl_via_ec : 1; + bool ctrl_ps2_aux_port : 1; + bool usb_charging : 1; + bool ymc_ec_trigger : 1; + } features; + struct { + bool initialized; + int type; + struct led_classdev led; + unsigned int last_brightness; + } kbd_bl; + struct { + bool initialized; + struct led_classdev led; + unsigned int last_brightness; + } fn_lock; +}; + +static bool no_bt_rfkill; +module_param(no_bt_rfkill, bool, 0444); +MODULE_PARM_DESC(no_bt_rfkill, "No rfkill for bluetooth."); + +static bool allow_v4_dytc; +module_param(allow_v4_dytc, bool, 0444); +MODULE_PARM_DESC(allow_v4_dytc, + "Enable DYTC version 4 platform-profile support. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool hw_rfkill_switch; +module_param(hw_rfkill_switch, bool, 0444); +MODULE_PARM_DESC(hw_rfkill_switch, + "Enable rfkill support for laptops with a hw on/off wifi switch/slider. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool set_fn_lock_led; +module_param(set_fn_lock_led, bool, 0444); +MODULE_PARM_DESC(set_fn_lock_led, + "Enable driver based updates of the fn-lock LED on fn-lock changes. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool ctrl_ps2_aux_port; +module_param(ctrl_ps2_aux_port, bool, 0444); +MODULE_PARM_DESC(ctrl_ps2_aux_port, + "Enable driver based PS/2 aux port en-/dis-abling on touchpad on/off toggle. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +static bool touchpad_ctrl_via_ec; +module_param(touchpad_ctrl_via_ec, bool, 0444); +MODULE_PARM_DESC(touchpad_ctrl_via_ec, + "Enable registering a 'touchpad' sysfs-attribute which can be used to manually " + "tell the EC to enable/disable the touchpad. This may not work on all models."); + +static bool ymc_ec_trigger __read_mostly; +module_param(ymc_ec_trigger, bool, 0444); +MODULE_PARM_DESC(ymc_ec_trigger, + "Enable EC triggering work-around to force emitting tablet mode events. " + "If you need this please report this to: platform-driver-x86@vger.kernel.org"); + +/* + * shared data + */ + +static struct ideapad_private *ideapad_shared; +static DEFINE_MUTEX(ideapad_shared_mutex); + +static int ideapad_shared_init(struct ideapad_private *priv) +{ + int ret; + + guard(mutex)(&ideapad_shared_mutex); + + if (!ideapad_shared) { + ideapad_shared = priv; + ret = 0; + } else { + dev_warn(&priv->adev->dev, "found multiple platform devices\n"); + ret = -EINVAL; + } + + return ret; +} + +static void ideapad_shared_exit(struct ideapad_private *priv) +{ + guard(mutex)(&ideapad_shared_mutex); + + if (ideapad_shared == priv) + ideapad_shared = NULL; +} + +/* + * ACPI Helpers + */ +#define IDEAPAD_EC_TIMEOUT 200 /* in ms */ + +static int eval_int(acpi_handle handle, const char *name, unsigned long *res) +{ + unsigned long long result; + acpi_status status; + + status = acpi_evaluate_integer(handle, (char *)name, NULL, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + *res = result; + + return 0; +} + +static int eval_int_with_arg(acpi_handle handle, const char *name, unsigned long arg, + unsigned long *res) +{ + struct acpi_object_list params; + unsigned long long result; + union acpi_object in_obj; + acpi_status status; + + params.count = 1; + params.pointer = &in_obj; + in_obj.type = ACPI_TYPE_INTEGER; + in_obj.integer.value = arg; + + status = acpi_evaluate_integer(handle, (char *)name, ¶ms, &result); + if (ACPI_FAILURE(status)) + return -EIO; + + if (res) + *res = result; + + return 0; +} + +static int exec_simple_method(acpi_handle handle, const char *name, unsigned long arg) +{ + acpi_status status = acpi_execute_simple_method(handle, (char *)name, arg); + + return ACPI_FAILURE(status) ? -EIO : 0; +} + +static int eval_gbmd(acpi_handle handle, unsigned long *res) +{ + return eval_int(handle, "GBMD", res); +} + +static int exec_sbmc(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "SBMC", arg); +} + +static int eval_hals(acpi_handle handle, unsigned long *res) +{ + return eval_int(handle, "HALS", res); +} + +static int exec_sals(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "SALS", arg); +} + +static int exec_kblc(acpi_handle handle, unsigned long arg) +{ + return exec_simple_method(handle, "KBLC", arg); +} + +static int eval_kblc(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "KBLC", cmd, res); +} + +static int eval_dytc(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "DYTC", cmd, res); +} + +static int eval_vpcr(acpi_handle handle, unsigned long cmd, unsigned long *res) +{ + return eval_int_with_arg(handle, "VPCR", cmd, res); +} + +static int eval_vpcw(acpi_handle handle, unsigned long cmd, unsigned long data) +{ + struct acpi_object_list params; + union acpi_object in_obj[2]; + acpi_status status; + + params.count = 2; + params.pointer = in_obj; + in_obj[0].type = ACPI_TYPE_INTEGER; + in_obj[0].integer.value = cmd; + in_obj[1].type = ACPI_TYPE_INTEGER; + in_obj[1].integer.value = data; + + status = acpi_evaluate_object(handle, "VPCW", ¶ms, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return 0; +} + +static int read_ec_data(acpi_handle handle, unsigned long cmd, unsigned long *data) +{ + unsigned long end_jiffies, val; + int err; + + err = eval_vpcw(handle, 1, cmd); + if (err) + return err; + + end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; + + while (time_before(jiffies, end_jiffies)) { + schedule(); + + err = eval_vpcr(handle, 1, &val); + if (err) + return err; + + if (val == 0) + return eval_vpcr(handle, 0, data); + } + + acpi_handle_err(handle, "timeout in %s\n", __func__); + + return -ETIMEDOUT; +} + +static int write_ec_cmd(acpi_handle handle, unsigned long cmd, unsigned long data) +{ + unsigned long end_jiffies, val; + int err; + + err = eval_vpcw(handle, 0, data); + if (err) + return err; + + err = eval_vpcw(handle, 1, cmd); + if (err) + return err; + + end_jiffies = jiffies + msecs_to_jiffies(IDEAPAD_EC_TIMEOUT) + 1; + + while (time_before(jiffies, end_jiffies)) { + schedule(); + + err = eval_vpcr(handle, 1, &val); + if (err) + return err; + + if (val == 0) + return 0; + } + + acpi_handle_err(handle, "timeout in %s\n", __func__); + + return -ETIMEDOUT; +} + +/* + * debugfs + */ +static int debugfs_status_show(struct seq_file *s, void *data) +{ + struct ideapad_private *priv = s->private; + unsigned long value; + + guard(mutex)(&priv->vpc_mutex); + + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &value)) + seq_printf(s, "Backlight max: %lu\n", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL, &value)) + seq_printf(s, "Backlight now: %lu\n", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &value)) + seq_printf(s, "BL power value: %s (%lu)\n", value ? "on" : "off", value); + + seq_puts(s, "=====================\n"); + + if (!read_ec_data(priv->adev->handle, VPCCMD_R_RF, &value)) + seq_printf(s, "Radio status: %s (%lu)\n", value ? "on" : "off", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_WIFI, &value)) + seq_printf(s, "Wifi status: %s (%lu)\n", value ? "on" : "off", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_BT, &value)) + seq_printf(s, "BT status: %s (%lu)\n", value ? "on" : "off", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_3G, &value)) + seq_printf(s, "3G status: %s (%lu)\n", value ? "on" : "off", value); + + seq_puts(s, "=====================\n"); + + if (!read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value)) + seq_printf(s, "Touchpad status: %s (%lu)\n", value ? "on" : "off", value); + if (!read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &value)) + seq_printf(s, "Camera status: %s (%lu)\n", value ? "on" : "off", value); + + seq_puts(s, "=====================\n"); + + if (!eval_gbmd(priv->adev->handle, &value)) + seq_printf(s, "GBMD: %#010lx\n", value); + if (!eval_hals(priv->adev->handle, &value)) + seq_printf(s, "HALS: %#010lx\n", value); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(debugfs_status); + +static int debugfs_cfg_show(struct seq_file *s, void *data) +{ + struct ideapad_private *priv = s->private; + + seq_printf(s, "_CFG: %#010lx\n\n", priv->cfg); + + seq_puts(s, "Capabilities:"); + if (test_bit(CFG_CAP_BT_BIT, &priv->cfg)) + seq_puts(s, " bluetooth"); + if (test_bit(CFG_CAP_3G_BIT, &priv->cfg)) + seq_puts(s, " 3G"); + if (test_bit(CFG_CAP_WIFI_BIT, &priv->cfg)) + seq_puts(s, " wifi"); + if (test_bit(CFG_CAP_CAM_BIT, &priv->cfg)) + seq_puts(s, " camera"); + seq_puts(s, "\n"); + + seq_puts(s, "OSD support:"); + if (test_bit(CFG_OSD_NUMLK_BIT, &priv->cfg)) + seq_puts(s, " num-lock"); + if (test_bit(CFG_OSD_CAPSLK_BIT, &priv->cfg)) + seq_puts(s, " caps-lock"); + if (test_bit(CFG_OSD_MICMUTE_BIT, &priv->cfg)) + seq_puts(s, " mic-mute"); + if (test_bit(CFG_OSD_TOUCHPAD_BIT, &priv->cfg)) + seq_puts(s, " touchpad"); + if (test_bit(CFG_OSD_CAM_BIT, &priv->cfg)) + seq_puts(s, " camera"); + seq_puts(s, "\n"); + + seq_puts(s, "Graphics: "); + switch (priv->cfg & 0x700) { + case 0x100: + seq_puts(s, "Intel"); + break; + case 0x200: + seq_puts(s, "ATI"); + break; + case 0x300: + seq_puts(s, "Nvidia"); + break; + case 0x400: + seq_puts(s, "Intel and ATI"); + break; + case 0x500: + seq_puts(s, "Intel and Nvidia"); + break; + } + seq_puts(s, "\n"); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(debugfs_cfg); + +static void ideapad_debugfs_init(struct ideapad_private *priv) +{ + struct dentry *dir; + + dir = debugfs_create_dir("ideapad", NULL); + priv->debug = dir; + + debugfs_create_file("cfg", 0444, dir, priv, &debugfs_cfg_fops); + debugfs_create_file("status", 0444, dir, priv, &debugfs_status_fops); +} + +static void ideapad_debugfs_exit(struct ideapad_private *priv) +{ + debugfs_remove_recursive(priv->debug); + priv->debug = NULL; +} + +/* + * sysfs + */ +static ssize_t camera_power_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_CAMERA, &result); + if (err) + return err; + } + + return sysfs_emit(buf, "%d\n", !!result); +} + +static ssize_t camera_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_CAMERA, state); + if (err) + return err; + } + + return count; +} + +static DEVICE_ATTR_RW(camera_power); + +static ssize_t conservation_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result; + int err; + + err = eval_gbmd(priv->adev->handle, &result); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", !!test_bit(GBMD_CONSERVATION_STATE_BIT, &result)); +} + +static ssize_t conservation_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + err = exec_sbmc(priv->adev->handle, state ? SBMC_CONSERVATION_ON : SBMC_CONSERVATION_OFF); + if (err) + return err; + + return count; +} + +static DEVICE_ATTR_RW(conservation_mode); + +static ssize_t fan_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_FAN, &result); + if (err) + return err; + } + + return sysfs_emit(buf, "%lu\n", result); +} + +static ssize_t fan_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned int state; + int err; + + err = kstrtouint(buf, 0, &state); + if (err) + return err; + + if (state > 4 || state == 3) + return -EINVAL; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_FAN, state); + if (err) + return err; + } + + return count; +} + +static DEVICE_ATTR_RW(fan_mode); + +static int ideapad_fn_lock_get(struct ideapad_private *priv) +{ + unsigned long hals; + int err; + + err = eval_hals(priv->adev->handle, &hals); + if (err) + return err; + + return !!test_bit(HALS_FNLOCK_STATE_BIT, &hals); +} + +static int ideapad_fn_lock_set(struct ideapad_private *priv, bool state) +{ + return exec_sals(priv->adev->handle, + state ? SALS_FNLOCK_ON : SALS_FNLOCK_OFF); +} + +static void ideapad_fn_lock_led_notify(struct ideapad_private *priv, int brightness) +{ + if (!priv->fn_lock.initialized) + return; + + if (brightness == priv->fn_lock.last_brightness) + return; + + priv->fn_lock.last_brightness = brightness; + + led_classdev_notify_brightness_hw_changed(&priv->fn_lock.led, brightness); +} + +static ssize_t fn_lock_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + int brightness; + + brightness = ideapad_fn_lock_get(priv); + if (brightness < 0) + return brightness; + + return sysfs_emit(buf, "%d\n", brightness); +} + +static ssize_t fn_lock_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + err = ideapad_fn_lock_set(priv, state); + if (err) + return err; + + ideapad_fn_lock_led_notify(priv, state); + + return count; +} + +static DEVICE_ATTR_RW(fn_lock); + +static ssize_t touchpad_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long result = 0; + int err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &result); + if (err) + return err; + } + + priv->r_touchpad_val = result; + + return sysfs_emit(buf, "%d\n", !!result); +} + +static ssize_t touchpad_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + scoped_guard(mutex, &priv->vpc_mutex) { + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_TOUCHPAD, state); + if (err) + return err; + } + + priv->r_touchpad_val = state; + + return count; +} + +static DEVICE_ATTR_RW(touchpad); + +static ssize_t usb_charging_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + unsigned long hals; + int err; + + err = eval_hals(priv->adev->handle, &hals); + if (err) + return err; + + return sysfs_emit(buf, "%d\n", !!test_bit(HALS_USB_CHARGING_STATE_BIT, &hals)); +} + +static ssize_t usb_charging_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + bool state; + int err; + + err = kstrtobool(buf, &state); + if (err) + return err; + + err = exec_sals(priv->adev->handle, state ? SALS_USB_CHARGING_ON : SALS_USB_CHARGING_OFF); + if (err) + return err; + + return count; +} + +static DEVICE_ATTR_RW(usb_charging); + +static struct attribute *ideapad_attributes[] = { + &dev_attr_camera_power.attr, + &dev_attr_conservation_mode.attr, + &dev_attr_fan_mode.attr, + &dev_attr_fn_lock.attr, + &dev_attr_touchpad.attr, + &dev_attr_usb_charging.attr, + NULL +}; + +static umode_t ideapad_is_visible(struct kobject *kobj, + struct attribute *attr, + int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct ideapad_private *priv = dev_get_drvdata(dev); + bool supported = true; + + if (attr == &dev_attr_camera_power.attr) + supported = test_bit(CFG_CAP_CAM_BIT, &priv->cfg); + else if (attr == &dev_attr_conservation_mode.attr) + supported = priv->features.conservation_mode; + else if (attr == &dev_attr_fan_mode.attr) + supported = priv->features.fan_mode; + else if (attr == &dev_attr_fn_lock.attr) + supported = priv->features.fn_lock; + else if (attr == &dev_attr_touchpad.attr) + supported = priv->features.touchpad_ctrl_via_ec; + else if (attr == &dev_attr_usb_charging.attr) + supported = priv->features.usb_charging; + + return supported ? attr->mode : 0; +} + +static const struct attribute_group ideapad_attribute_group = { + .is_visible = ideapad_is_visible, + .attrs = ideapad_attributes +}; +__ATTRIBUTE_GROUPS(ideapad_attribute); + +/* + * DYTC Platform profile + */ +#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ +#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ +#define DYTC_CMD_GET 2 /* To get current IC function and mode */ +#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ + +#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ +#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ +#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ + +#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ +#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ + +#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ +#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ +#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ + +#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ +#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ +#define DYTC_FUNCTION_MMC 11 /* Function = 11, desk mode */ + +#define DYTC_MODE_PERFORM 2 /* High power mode aka performance */ +#define DYTC_MODE_LOW_POWER 3 /* Low power mode aka quiet */ +#define DYTC_MODE_BALANCE 0xF /* Default mode aka balanced */ + +#define DYTC_SET_COMMAND(function, mode, on) \ + (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ + (mode) << DYTC_SET_MODE_BIT | \ + (on) << DYTC_SET_VALID_BIT) + +#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 0) + +#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_BALANCE, 1) + +static int convert_dytc_to_profile(int dytcmode, enum platform_profile_option *profile) +{ + switch (dytcmode) { + case DYTC_MODE_LOW_POWER: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case DYTC_MODE_BALANCE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case DYTC_MODE_PERFORM: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + default: /* Unknown mode */ + return -EINVAL; + } + + return 0; +} + +static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) +{ + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + *perfmode = DYTC_MODE_LOW_POWER; + break; + case PLATFORM_PROFILE_BALANCED: + *perfmode = DYTC_MODE_BALANCE; + break; + case PLATFORM_PROFILE_PERFORMANCE: + *perfmode = DYTC_MODE_PERFORM; + break; + default: /* Unknown profile */ + return -EOPNOTSUPP; + } + + return 0; +} + +/* + * dytc_profile_get: Function to register with platform_profile + * handler. Returns current platform profile. + */ +static int dytc_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); + + *profile = dytc->current_profile; + return 0; +} + +/* + * Helper function - check if we are in CQL mode and if we are + * - disable CQL, + * - run the command + * - enable CQL + * If not in CQL mode, just run the command + */ +static int dytc_cql_command(struct ideapad_private *priv, unsigned long cmd, + unsigned long *output) +{ + int err, cmd_err, cur_funcmode; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = eval_dytc(priv->adev->handle, DYTC_CMD_GET, output); + if (err) + return err; + + cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; + /* Check if we're OK to return immediately */ + if (cmd == DYTC_CMD_GET && cur_funcmode != DYTC_FUNCTION_CQL) + return 0; + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = eval_dytc(priv->adev->handle, DYTC_DISABLE_CQL, NULL); + if (err) + return err; + } + + cmd_err = eval_dytc(priv->adev->handle, cmd, output); + /* Check return condition after we've restored CQL state */ + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = eval_dytc(priv->adev->handle, DYTC_ENABLE_CQL, NULL); + if (err) + return err; + } + + return cmd_err; +} + +/* + * dytc_profile_set: Function to register with platform_profile + * handler. Sets current platform profile. + */ +static int dytc_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ideapad_dytc_priv *dytc = dev_get_drvdata(dev); + struct ideapad_private *priv = dytc->priv; + unsigned long output; + int err; + + scoped_guard(mutex_intr, &dytc->mutex) { + if (profile == PLATFORM_PROFILE_BALANCED) { + /* To get back to balanced mode we just issue a reset command */ + err = eval_dytc(priv->adev->handle, DYTC_CMD_RESET, NULL); + if (err) + return err; + } else { + int perfmode; + + err = convert_profile_to_dytc(profile, &perfmode); + if (err) + return err; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_cql_command(priv, + DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), + &output); + if (err) + return err; + } + + /* Success - update current profile */ + dytc->current_profile = profile; + return 0; + } + + return -EINTR; +} + +static int dytc_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + + return 0; +} + +static void dytc_profile_refresh(struct ideapad_private *priv) +{ + enum platform_profile_option profile; + unsigned long output; + int err, perfmode; + + scoped_guard(mutex, &priv->dytc->mutex) + err = dytc_cql_command(priv, DYTC_CMD_GET, &output); + if (err) + return; + + perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; + + if (convert_dytc_to_profile(perfmode, &profile)) + return; + + if (profile != priv->dytc->current_profile) { + priv->dytc->current_profile = profile; + platform_profile_notify(priv->dytc->ppdev); + } +} + +static const struct dmi_system_id ideapad_dytc_v4_allow_table[] = { + { + /* Ideapad 5 Pro 16ACH6 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82L5") + } + }, + { + /* Ideapad 5 15ITL05 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "IdeaPad 5 15ITL05") + } + }, + {} +}; + +static const struct platform_profile_ops dytc_profile_ops = { + .probe = dytc_profile_probe, + .profile_get = dytc_profile_get, + .profile_set = dytc_profile_set, +}; + +static int ideapad_dytc_profile_init(struct ideapad_private *priv) +{ + int err, dytc_version; + unsigned long output; + + if (!priv->features.dytc) + return -ENODEV; + + err = eval_dytc(priv->adev->handle, DYTC_CMD_QUERY, &output); + /* For all other errors we can flag the failure */ + if (err) + return err; + + /* Check DYTC is enabled and supports mode setting */ + if (!test_bit(DYTC_QUERY_ENABLE_BIT, &output)) { + dev_info(&priv->platform_device->dev, "DYTC_QUERY_ENABLE_BIT returned false\n"); + return -ENODEV; + } + + dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; + + if (dytc_version < 4) { + dev_info(&priv->platform_device->dev, "DYTC_VERSION < 4 is not supported\n"); + return -ENODEV; + } + + if (dytc_version < 5 && + !(allow_v4_dytc || dmi_check_system(ideapad_dytc_v4_allow_table))) { + dev_info(&priv->platform_device->dev, + "DYTC_VERSION 4 support may not work. Pass ideapad_laptop.allow_v4_dytc=Y on the kernel commandline to enable\n"); + return -ENODEV; + } + + priv->dytc = kzalloc(sizeof(*priv->dytc), GFP_KERNEL); + if (!priv->dytc) + return -ENOMEM; + + mutex_init(&priv->dytc->mutex); + + priv->dytc->priv = priv; + + /* Create platform_profile structure and register */ + priv->dytc->ppdev = devm_platform_profile_register(&priv->platform_device->dev, + "ideapad-laptop", priv->dytc, + &dytc_profile_ops); + if (IS_ERR(priv->dytc->ppdev)) { + err = PTR_ERR(priv->dytc->ppdev); + goto pp_reg_failed; + } + + /* Ensure initial values are correct */ + dytc_profile_refresh(priv); + + return 0; + +pp_reg_failed: + mutex_destroy(&priv->dytc->mutex); + kfree(priv->dytc); + priv->dytc = NULL; + + return err; +} + +static void ideapad_dytc_profile_exit(struct ideapad_private *priv) +{ + if (!priv->dytc) + return; + + mutex_destroy(&priv->dytc->mutex); + kfree(priv->dytc); + + priv->dytc = NULL; +} + +/* + * Rfkill + */ +struct ideapad_rfk_data { + char *name; + int cfgbit; + int opcode; + int type; +}; + +static const struct ideapad_rfk_data ideapad_rfk_data[] = { + { "ideapad_wlan", CFG_CAP_WIFI_BIT, VPCCMD_W_WIFI, RFKILL_TYPE_WLAN }, + { "ideapad_bluetooth", CFG_CAP_BT_BIT, VPCCMD_W_BT, RFKILL_TYPE_BLUETOOTH }, + { "ideapad_3g", CFG_CAP_3G_BIT, VPCCMD_W_3G, RFKILL_TYPE_WWAN }, +}; + +static int ideapad_rfk_set(void *data, bool blocked) +{ + struct ideapad_rfk_priv *priv = data; + int opcode = ideapad_rfk_data[priv->dev].opcode; + + guard(mutex)(&priv->priv->vpc_mutex); + + return write_ec_cmd(priv->priv->adev->handle, opcode, !blocked); +} + +static const struct rfkill_ops ideapad_rfk_ops = { + .set_block = ideapad_rfk_set, +}; + +static void ideapad_sync_rfk_state(struct ideapad_private *priv) +{ + unsigned long hw_blocked = 0; + int i; + + if (priv->features.hw_rfkill_switch) { + guard(mutex)(&priv->vpc_mutex); + + if (read_ec_data(priv->adev->handle, VPCCMD_R_RF, &hw_blocked)) + return; + hw_blocked = !hw_blocked; + } + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + if (priv->rfk[i]) + rfkill_set_hw_state(priv->rfk[i], hw_blocked); +} + +static int ideapad_register_rfkill(struct ideapad_private *priv, int dev) +{ + unsigned long rf_enabled; + int err; + + if (no_bt_rfkill && ideapad_rfk_data[dev].type == RFKILL_TYPE_BLUETOOTH) { + /* Force to enable bluetooth when no_bt_rfkill=1 */ + write_ec_cmd(priv->adev->handle, ideapad_rfk_data[dev].opcode, 1); + return 0; + } + + priv->rfk_priv[dev].dev = dev; + priv->rfk_priv[dev].priv = priv; + + priv->rfk[dev] = rfkill_alloc(ideapad_rfk_data[dev].name, + &priv->platform_device->dev, + ideapad_rfk_data[dev].type, + &ideapad_rfk_ops, + &priv->rfk_priv[dev]); + if (!priv->rfk[dev]) + return -ENOMEM; + + err = read_ec_data(priv->adev->handle, ideapad_rfk_data[dev].opcode - 1, &rf_enabled); + if (err) + rf_enabled = 1; + + rfkill_init_sw_state(priv->rfk[dev], !rf_enabled); + + err = rfkill_register(priv->rfk[dev]); + if (err) + rfkill_destroy(priv->rfk[dev]); + + return err; +} + +static void ideapad_unregister_rfkill(struct ideapad_private *priv, int dev) +{ + if (!priv->rfk[dev]) + return; + + rfkill_unregister(priv->rfk[dev]); + rfkill_destroy(priv->rfk[dev]); +} + +/* + * input device + */ +#define IDEAPAD_WMI_KEY 0x100 + +static const struct key_entry ideapad_keymap[] = { + { KE_KEY, 6, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, 7, { KEY_CAMERA } }, + { KE_KEY, 8, { KEY_MICMUTE } }, + { KE_KEY, 11, { KEY_F16 } }, + { KE_KEY, 13, { KEY_WLAN } }, + { KE_KEY, 16, { KEY_PROG1 } }, + { KE_KEY, 17, { KEY_PROG2 } }, + { KE_KEY, 64, { KEY_PROG3 } }, + { KE_KEY, 65, { KEY_PROG4 } }, + { KE_KEY, 66, { KEY_TOUCHPAD_OFF } }, + { KE_KEY, 67, { KEY_TOUCHPAD_ON } }, + { KE_KEY, 128, { KEY_ESC } }, + + /* + * WMI keys + */ + + /* FnLock (handled by the firmware) */ + { KE_IGNORE, 0x02 | IDEAPAD_WMI_KEY }, + /* Esc (handled by the firmware) */ + { KE_IGNORE, 0x03 | IDEAPAD_WMI_KEY }, + /* Customizable Lenovo Hotkey ("star" with 'S' inside) */ + { KE_KEY, 0x01 | IDEAPAD_WMI_KEY, { KEY_FAVORITES } }, + { KE_KEY, 0x04 | IDEAPAD_WMI_KEY, { KEY_SELECTIVE_SCREENSHOT } }, + /* Lenovo Support */ + { KE_KEY, 0x07 | IDEAPAD_WMI_KEY, { KEY_HELP } }, + { KE_KEY, 0x0e | IDEAPAD_WMI_KEY, { KEY_PICKUP_PHONE } }, + { KE_KEY, 0x0f | IDEAPAD_WMI_KEY, { KEY_HANGUP_PHONE } }, + /* Refresh Rate Toggle (Fn+R) */ + { KE_KEY, 0x10 | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, + /* Dark mode toggle */ + { KE_KEY, 0x13 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, + /* Sound profile switch */ + { KE_KEY, 0x12 | IDEAPAD_WMI_KEY, { KEY_PROG2 } }, + /* Lenovo Virtual Background application */ + { KE_KEY, 0x28 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, + /* Lenovo Support */ + { KE_KEY, 0x27 | IDEAPAD_WMI_KEY, { KEY_HELP } }, + /* Refresh Rate Toggle */ + { KE_KEY, 0x0a | IDEAPAD_WMI_KEY, { KEY_REFRESH_RATE_TOGGLE } }, + /* Specific to some newer models */ + { KE_KEY, 0x3e | IDEAPAD_WMI_KEY, { KEY_MICMUTE } }, + { KE_KEY, 0x3f | IDEAPAD_WMI_KEY, { KEY_RFKILL } }, + /* Star- (User Assignable Key) */ + { KE_KEY, 0x44 | IDEAPAD_WMI_KEY, { KEY_PROG1 } }, + /* Eye */ + { KE_KEY, 0x45 | IDEAPAD_WMI_KEY, { KEY_PROG3 } }, + /* Performance toggle also Fn+Q, handled inside ideapad_wmi_notify() */ + { KE_KEY, 0x3d | IDEAPAD_WMI_KEY, { KEY_PROG4 } }, + /* shift + prtsc */ + { KE_KEY, 0x2d | IDEAPAD_WMI_KEY, { KEY_CUT } }, + { KE_KEY, 0x29 | IDEAPAD_WMI_KEY, { KEY_TOUCHPAD_TOGGLE } }, + { KE_KEY, 0x2a | IDEAPAD_WMI_KEY, { KEY_ROOT_MENU } }, + + { KE_END }, +}; + +static int ideapad_input_init(struct ideapad_private *priv) +{ + struct input_dev *inputdev; + int err; + + inputdev = input_allocate_device(); + if (!inputdev) + return -ENOMEM; + + inputdev->name = "Ideapad extra buttons"; + inputdev->phys = "ideapad/input0"; + inputdev->id.bustype = BUS_HOST; + inputdev->dev.parent = &priv->platform_device->dev; + + err = sparse_keymap_setup(inputdev, ideapad_keymap, NULL); + if (err) { + dev_err(&priv->platform_device->dev, + "Could not set up input device keymap: %d\n", err); + goto err_free_dev; + } + + err = input_register_device(inputdev); + if (err) { + dev_err(&priv->platform_device->dev, + "Could not register input device: %d\n", err); + goto err_free_dev; + } + + priv->inputdev = inputdev; + + return 0; + +err_free_dev: + input_free_device(inputdev); + + return err; +} + +static void ideapad_input_exit(struct ideapad_private *priv) +{ + input_unregister_device(priv->inputdev); + priv->inputdev = NULL; +} + +static void ideapad_input_report(struct ideapad_private *priv, + unsigned long scancode) +{ + sparse_keymap_report_event(priv->inputdev, scancode, 1, true); +} + +static void ideapad_input_novokey(struct ideapad_private *priv) +{ + unsigned long long_pressed; + + scoped_guard(mutex, &priv->vpc_mutex) + if (read_ec_data(priv->adev->handle, VPCCMD_R_NOVO, &long_pressed)) + return; + + if (long_pressed) + ideapad_input_report(priv, 17); + else + ideapad_input_report(priv, 16); +} + +static void ideapad_check_special_buttons(struct ideapad_private *priv) +{ + unsigned long bit, value; + + scoped_guard(mutex, &priv->vpc_mutex) + if (read_ec_data(priv->adev->handle, VPCCMD_R_SPECIAL_BUTTONS, &value)) + return; + + for_each_set_bit (bit, &value, 16) { + switch (bit) { + case 6: /* Z570 */ + case 0: /* Z580 */ + /* Thermal Management / Performance Mode button */ + if (priv->dytc) + platform_profile_cycle(); + else + ideapad_input_report(priv, 65); + break; + case 1: + /* OneKey Theater button */ + ideapad_input_report(priv, 64); + break; + default: + dev_info(&priv->platform_device->dev, + "Unknown special button: %lu\n", bit); + break; + } + } +} + +/* + * backlight + */ +static int ideapad_backlight_get_brightness(struct backlight_device *blightdev) +{ + struct ideapad_private *priv = bl_get_data(blightdev); + unsigned long now; + int err; + + guard(mutex)(&priv->vpc_mutex); + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + if (err) + return err; + + return now; +} + +static int ideapad_backlight_update_status(struct backlight_device *blightdev) +{ + struct ideapad_private *priv = bl_get_data(blightdev); + int err; + + guard(mutex)(&priv->vpc_mutex); + + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL, + blightdev->props.brightness); + if (err) + return err; + + err = write_ec_cmd(priv->adev->handle, VPCCMD_W_BL_POWER, + blightdev->props.power != BACKLIGHT_POWER_OFF); + if (err) + return err; + + return 0; +} + +static const struct backlight_ops ideapad_backlight_ops = { + .get_brightness = ideapad_backlight_get_brightness, + .update_status = ideapad_backlight_update_status, +}; + +static int ideapad_backlight_init(struct ideapad_private *priv) +{ + struct backlight_device *blightdev; + struct backlight_properties props; + unsigned long max, now, power; + int err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_MAX, &max); + if (err) + return err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + if (err) + return err; + + err = read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power); + if (err) + return err; + + memset(&props, 0, sizeof(props)); + + props.max_brightness = max; + props.type = BACKLIGHT_PLATFORM; + + blightdev = backlight_device_register("ideapad", + &priv->platform_device->dev, + priv, + &ideapad_backlight_ops, + &props); + if (IS_ERR(blightdev)) { + err = PTR_ERR(blightdev); + dev_err(&priv->platform_device->dev, + "Could not register backlight device: %d\n", err); + return err; + } + + priv->blightdev = blightdev; + blightdev->props.brightness = now; + blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; + + backlight_update_status(blightdev); + + return 0; +} + +static void ideapad_backlight_exit(struct ideapad_private *priv) +{ + backlight_device_unregister(priv->blightdev); + priv->blightdev = NULL; +} + +static void ideapad_backlight_notify_power(struct ideapad_private *priv) +{ + struct backlight_device *blightdev = priv->blightdev; + unsigned long power; + + if (!blightdev) + return; + + guard(mutex)(&priv->vpc_mutex); + + if (read_ec_data(priv->adev->handle, VPCCMD_R_BL_POWER, &power)) + return; + + blightdev->props.power = power ? BACKLIGHT_POWER_ON : BACKLIGHT_POWER_OFF; +} + +static void ideapad_backlight_notify_brightness(struct ideapad_private *priv) +{ + unsigned long now; + + /* if we control brightness via acpi video driver */ + if (!priv->blightdev) + scoped_guard(mutex, &priv->vpc_mutex) + read_ec_data(priv->adev->handle, VPCCMD_R_BL, &now); + else + backlight_force_update(priv->blightdev, BACKLIGHT_UPDATE_HOTKEY); +} + +/* + * keyboard backlight + */ +static int ideapad_kbd_bl_check_tristate(int type) +{ + return (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO); +} + +static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv) +{ + unsigned long value; + int err; + + if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) { + err = eval_kblc(priv->adev->handle, + FIELD_PREP(KBD_BL_COMMAND_TYPE, priv->kbd_bl.type) | + KBD_BL_COMMAND_GET, + &value); + + if (err) + return err; + + /* Convert returned value to brightness level */ + value = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value); + + /* Off, low or high */ + if (value <= priv->kbd_bl.led.max_brightness) + return value; + + /* Auto, report as off */ + if (value == priv->kbd_bl.led.max_brightness + 1) + return 0; + + /* Unknown value */ + dev_warn(&priv->platform_device->dev, + "Unknown keyboard backlight value: %lu", value); + return -EINVAL; + } + + err = eval_hals(priv->adev->handle, &value); + if (err) + return err; + + return !!test_bit(HALS_KBD_BL_STATE_BIT, &value); +} + +static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); + + return ideapad_kbd_bl_brightness_get(priv); +} + +static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness) +{ + int err; + unsigned long value; + int type = priv->kbd_bl.type; + + if (ideapad_kbd_bl_check_tristate(type)) { + if (brightness > priv->kbd_bl.led.max_brightness) + return -EINVAL; + + value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) | + FIELD_PREP(KBD_BL_COMMAND_TYPE, type) | + KBD_BL_COMMAND_SET; + err = exec_kblc(priv->adev->handle, value); + } else { + err = exec_sals(priv->adev->handle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF); + } + + if (err) + return err; + + priv->kbd_bl.last_brightness = brightness; + + return 0; +} + +static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led); + + return ideapad_kbd_bl_brightness_set(priv, brightness); +} + +static void ideapad_kbd_bl_notify(struct ideapad_private *priv) +{ + int brightness; + + if (!priv->kbd_bl.initialized) + return; + + brightness = ideapad_kbd_bl_brightness_get(priv); + if (brightness < 0) + return; + + if (brightness == priv->kbd_bl.last_brightness) + return; + + priv->kbd_bl.last_brightness = brightness; + + led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness); +} + +static int ideapad_kbd_bl_init(struct ideapad_private *priv) +{ + int brightness, err; + + if (!priv->features.kbd_bl) + return -ENODEV; + + if (WARN_ON(priv->kbd_bl.initialized)) + return -EEXIST; + + if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type)) { + priv->kbd_bl.led.max_brightness = 2; + } else { + priv->kbd_bl.led.max_brightness = 1; + } + + brightness = ideapad_kbd_bl_brightness_get(priv); + if (brightness < 0) + return brightness; + + priv->kbd_bl.last_brightness = brightness; + priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT; + priv->kbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get; + priv->kbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set; + priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED; + + err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led); + if (err) + return err; + + priv->kbd_bl.initialized = true; + + return 0; +} + +static void ideapad_kbd_bl_exit(struct ideapad_private *priv) +{ + if (!priv->kbd_bl.initialized) + return; + + priv->kbd_bl.initialized = false; + + led_classdev_unregister(&priv->kbd_bl.led); +} + +/* + * FnLock LED + */ +static enum led_brightness ideapad_fn_lock_led_cdev_get(struct led_classdev *led_cdev) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); + + return ideapad_fn_lock_get(priv); +} + +static int ideapad_fn_lock_led_cdev_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, fn_lock.led); + + return ideapad_fn_lock_set(priv, brightness); +} + +static int ideapad_fn_lock_led_init(struct ideapad_private *priv) +{ + int brightness, err; + + if (!priv->features.fn_lock) + return -ENODEV; + + if (WARN_ON(priv->fn_lock.initialized)) + return -EEXIST; + + priv->fn_lock.led.max_brightness = 1; + + brightness = ideapad_fn_lock_get(priv); + if (brightness < 0) + return brightness; + + priv->fn_lock.last_brightness = brightness; + priv->fn_lock.led.name = "platform::" LED_FUNCTION_FNLOCK; + priv->fn_lock.led.brightness_get = ideapad_fn_lock_led_cdev_get; + priv->fn_lock.led.brightness_set_blocking = ideapad_fn_lock_led_cdev_set; + priv->fn_lock.led.flags = LED_BRIGHT_HW_CHANGED; + + err = led_classdev_register(&priv->platform_device->dev, &priv->fn_lock.led); + if (err) + return err; + + priv->fn_lock.initialized = true; + + return 0; +} + +static void ideapad_fn_lock_led_exit(struct ideapad_private *priv) +{ + if (!priv->fn_lock.initialized) + return; + + priv->fn_lock.initialized = false; + + led_classdev_unregister(&priv->fn_lock.led); +} + +/* + * module init/exit + */ +static void ideapad_sync_touchpad_state(struct ideapad_private *priv, bool send_events) +{ + unsigned long value; + unsigned char param; + int ret; + + /* Without reading from EC touchpad LED doesn't switch state */ + scoped_guard(mutex, &priv->vpc_mutex) + ret = read_ec_data(priv->adev->handle, VPCCMD_R_TOUCHPAD, &value); + if (ret) + return; + + /* + * Some IdeaPads don't really turn off touchpad - they only + * switch the LED state. We (de)activate KBC AUX port to turn + * touchpad off and on. We send KEY_TOUCHPAD_OFF and + * KEY_TOUCHPAD_ON to not to get out of sync with LED + */ + if (priv->features.ctrl_ps2_aux_port) + i8042_command(¶m, value ? I8042_CMD_AUX_ENABLE : I8042_CMD_AUX_DISABLE); + + /* + * On older models the EC controls the touchpad and toggles it on/off + * itself, in this case we report KEY_TOUCHPAD_ON/_OFF. Some models do + * an acpi-notify with VPC bit 5 set on resume, so this function get + * called with send_events=true on every resume. Therefor if the EC did + * not toggle, do nothing to avoid sending spurious KEY_TOUCHPAD_TOGGLE. + */ + if (send_events && value != priv->r_touchpad_val) { + ideapad_input_report(priv, value ? 67 : 66); + sysfs_notify(&priv->platform_device->dev.kobj, NULL, "touchpad"); + } + + priv->r_touchpad_val = value; +} + +static const struct dmi_system_id ymc_ec_trigger_quirk_dmi_table[] = { + { + /* Lenovo Yoga 7 14ARB7 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82QF"), + }, + }, + { + /* Lenovo Yoga 7 14ACN6 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82N7"), + }, + }, + { } +}; + +static void ideapad_laptop_trigger_ec(void) +{ + struct ideapad_private *priv; + int ret; + + guard(mutex)(&ideapad_shared_mutex); + + priv = ideapad_shared; + if (!priv) + return; + + if (!priv->features.ymc_ec_trigger) + return; + + scoped_guard(mutex, &priv->vpc_mutex) + ret = write_ec_cmd(priv->adev->handle, VPCCMD_W_YMC, 1); + if (ret) + dev_warn(&priv->platform_device->dev, "Could not write YMC: %d\n", ret); +} + +static int ideapad_laptop_nb_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + switch (action) { + case IDEAPAD_LAPTOP_YMC_EVENT: + ideapad_laptop_trigger_ec(); + break; + } + + return 0; +} + +static struct notifier_block ideapad_laptop_notifier = { + .notifier_call = ideapad_laptop_nb_notify, +}; + +static BLOCKING_NOTIFIER_HEAD(ideapad_laptop_chain_head); + +int ideapad_laptop_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&ideapad_laptop_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_register_notifier, "IDEAPAD_LAPTOP"); + +int ideapad_laptop_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&ideapad_laptop_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_unregister_notifier, "IDEAPAD_LAPTOP"); + +void ideapad_laptop_call_notifier(unsigned long action, void *data) +{ + blocking_notifier_call_chain(&ideapad_laptop_chain_head, action, data); +} +EXPORT_SYMBOL_NS_GPL(ideapad_laptop_call_notifier, "IDEAPAD_LAPTOP"); + +static void ideapad_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct ideapad_private *priv = data; + unsigned long vpc1, vpc2, bit; + + scoped_guard(mutex, &priv->vpc_mutex) { + if (read_ec_data(handle, VPCCMD_R_VPC1, &vpc1)) + return; + + if (read_ec_data(handle, VPCCMD_R_VPC2, &vpc2)) + return; + } + + vpc1 = (vpc2 << 8) | vpc1; + + for_each_set_bit (bit, &vpc1, 16) { + switch (bit) { + case 13: + case 11: + case 8: + case 7: + case 6: + ideapad_input_report(priv, bit); + break; + case 10: + /* + * This event gets send on a Yoga 300-11IBR when the EC + * believes that the device has changed between laptop/ + * tent/stand/tablet mode. The EC relies on getting + * angle info from 2 accelerometers through a special + * windows service calling a DSM on the DUAL250E ACPI- + * device. Linux does not do this, making the laptop/ + * tent/stand/tablet mode info unreliable, so we simply + * ignore these events. + */ + break; + case 9: + ideapad_sync_rfk_state(priv); + break; + case 5: + ideapad_sync_touchpad_state(priv, true); + break; + case 4: + ideapad_backlight_notify_brightness(priv); + break; + case 3: + ideapad_input_novokey(priv); + break; + case 2: + ideapad_backlight_notify_power(priv); + break; + case KBD_BL_KBLC_CHANGED_EVENT: + case 1: + /* + * Some IdeaPads report event 1 every ~20 + * seconds while on battery power; some + * report this when changing to/from tablet + * mode; some report this when the keyboard + * backlight has changed. + */ + ideapad_kbd_bl_notify(priv); + break; + case 0: + ideapad_check_special_buttons(priv); + break; + default: + dev_info(&priv->platform_device->dev, + "Unknown event: %lu\n", bit); + } + } +} + +/* On some models we need to call exec_sals(SALS_FNLOCK_ON/OFF) to set the LED */ +static const struct dmi_system_id set_fn_lock_led_list[] = { + { + /* https://bugzilla.kernel.org/show_bug.cgi?id=212671 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion R7000P2020H"), + } + }, + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo Legion 5 15ARH05"), + } + }, + {} +}; + +/* + * Some ideapads have a hardware rfkill switch, but most do not have one. + * Reading VPCCMD_R_RF always results in 0 on models without a hardware rfkill, + * switch causing ideapad_laptop to wrongly report all radios as hw-blocked. + * There used to be a long list of DMI ids for models without a hw rfkill + * switch here, but that resulted in playing whack a mole. + * More importantly wrongly reporting the wifi radio as hw-blocked, results in + * non working wifi. Whereas not reporting it hw-blocked, when it actually is + * hw-blocked results in an empty SSID list, which is a much more benign + * failure mode. + * So the default now is the much safer option of assuming there is no + * hardware rfkill switch. This default also actually matches most hardware, + * since having a hw rfkill switch is quite rare on modern hardware, so this + * also leads to a much shorter list. + */ +static const struct dmi_system_id hw_rfkill_list[] = { + {} +}; + +/* + * On some models the EC toggles the touchpad muted LED on touchpad toggle + * hotkey presses, but the EC does not actually disable the touchpad itself. + * On these models the driver needs to explicitly enable/disable the i8042 + * (PS/2) aux port. + */ +static const struct dmi_system_id ctrl_ps2_aux_port_list[] = { + { + /* Lenovo Ideapad Z570 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Ideapad Z570"), + }, + }, + {} +}; + +static void ideapad_check_features(struct ideapad_private *priv) +{ + acpi_handle handle = priv->adev->handle; + unsigned long val; + + priv->features.set_fn_lock_led = + set_fn_lock_led || dmi_check_system(set_fn_lock_led_list); + priv->features.hw_rfkill_switch = + hw_rfkill_switch || dmi_check_system(hw_rfkill_list); + priv->features.ctrl_ps2_aux_port = + ctrl_ps2_aux_port || dmi_check_system(ctrl_ps2_aux_port_list); + priv->features.touchpad_ctrl_via_ec = touchpad_ctrl_via_ec; + priv->features.ymc_ec_trigger = + ymc_ec_trigger || dmi_check_system(ymc_ec_trigger_quirk_dmi_table); + + if (!read_ec_data(handle, VPCCMD_R_FAN, &val)) + priv->features.fan_mode = true; + + if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) + priv->features.conservation_mode = true; + + if (acpi_has_method(handle, "DYTC")) + priv->features.dytc = true; + + if (acpi_has_method(handle, "HALS") && acpi_has_method(handle, "SALS")) { + if (!eval_hals(handle, &val)) { + if (test_bit(HALS_FNLOCK_SUPPORT_BIT, &val)) + priv->features.fn_lock = true; + + if (test_bit(HALS_KBD_BL_SUPPORT_BIT, &val)) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_STANDARD; + } + + if (test_bit(HALS_USB_CHARGING_SUPPORT_BIT, &val)) + priv->features.usb_charging = true; + } + } + + if (acpi_has_method(handle, "KBLC")) { + if (!eval_kblc(priv->adev->handle, KBD_BL_QUERY_TYPE, &val)) { + if (val == KBD_BL_TRISTATE_TYPE) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_TRISTATE; + } else if (val == KBD_BL_TRISTATE_AUTO_TYPE) { + priv->features.kbd_bl = true; + priv->kbd_bl.type = KBD_BL_TRISTATE_AUTO; + } else { + dev_warn(&priv->platform_device->dev, + "Unknown keyboard type: %lu", + val); + } + } + } +} + +#if IS_ENABLED(CONFIG_ACPI_WMI) +/* + * WMI driver + */ +enum ideapad_wmi_event_type { + IDEAPAD_WMI_EVENT_ESC, + IDEAPAD_WMI_EVENT_FN_KEYS, +}; + +struct ideapad_wmi_private { + enum ideapad_wmi_event_type event; +}; + +static int ideapad_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct ideapad_wmi_private *wpriv; + + wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL); + if (!wpriv) + return -ENOMEM; + + *wpriv = *(const struct ideapad_wmi_private *)context; + + dev_set_drvdata(&wdev->dev, wpriv); + return 0; +} + +static void ideapad_wmi_notify(struct wmi_device *wdev, union acpi_object *data) +{ + struct ideapad_wmi_private *wpriv = dev_get_drvdata(&wdev->dev); + struct ideapad_private *priv; + + guard(mutex)(&ideapad_shared_mutex); + + priv = ideapad_shared; + if (!priv) + return; + + switch (wpriv->event) { + case IDEAPAD_WMI_EVENT_ESC: + ideapad_input_report(priv, 128); + break; + case IDEAPAD_WMI_EVENT_FN_KEYS: + if (priv->features.set_fn_lock_led) { + int brightness = ideapad_fn_lock_get(priv); + + if (brightness >= 0) { + ideapad_fn_lock_set(priv, brightness); + ideapad_fn_lock_led_notify(priv, brightness); + } + } + + if (data->type != ACPI_TYPE_INTEGER) { + dev_warn(&wdev->dev, + "WMI event data is not an integer\n"); + break; + } + + dev_dbg(&wdev->dev, "WMI fn-key event: 0x%llx\n", + data->integer.value); + + /* performance button triggered by 0x3d */ + if (data->integer.value == 0x3d && priv->dytc) { + platform_profile_cycle(); + break; + } + + /* 0x02 FnLock, 0x03 Esc */ + if (data->integer.value == 0x02 || data->integer.value == 0x03) + ideapad_fn_lock_led_notify(priv, data->integer.value == 0x02); + + ideapad_input_report(priv, + data->integer.value | IDEAPAD_WMI_KEY); + + break; + } +} + +static const struct ideapad_wmi_private ideapad_wmi_context_esc = { + .event = IDEAPAD_WMI_EVENT_ESC +}; + +static const struct ideapad_wmi_private ideapad_wmi_context_fn_keys = { + .event = IDEAPAD_WMI_EVENT_FN_KEYS +}; + +static const struct wmi_device_id ideapad_wmi_ids[] = { + { "26CAB2E5-5CF1-46AE-AAC3-4A12B6BA50E6", &ideapad_wmi_context_esc }, /* Yoga 3 */ + { "56322276-8493-4CE8-A783-98C991274F5E", &ideapad_wmi_context_esc }, /* Yoga 700 */ + { "8FC0DE0C-B4E4-43FD-B0F3-8871711C1294", &ideapad_wmi_context_fn_keys }, /* Legion 5 */ + {}, +}; +MODULE_DEVICE_TABLE(wmi, ideapad_wmi_ids); + +static struct wmi_driver ideapad_wmi_driver = { + .driver = { + .name = "ideapad_wmi", + }, + .id_table = ideapad_wmi_ids, + .probe = ideapad_wmi_probe, + .notify = ideapad_wmi_notify, +}; + +static int ideapad_wmi_driver_register(void) +{ + return wmi_driver_register(&ideapad_wmi_driver); +} + +static void ideapad_wmi_driver_unregister(void) +{ + return wmi_driver_unregister(&ideapad_wmi_driver); +} + +#else +static inline int ideapad_wmi_driver_register(void) { return 0; } +static inline void ideapad_wmi_driver_unregister(void) { } +#endif + +/* + * ACPI driver + */ +static int ideapad_acpi_add(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct ideapad_private *priv; + acpi_status status; + unsigned long cfg; + int err, i; + + if (!adev || eval_int(adev->handle, "_CFG", &cfg)) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(&pdev->dev, priv); + + priv->cfg = cfg; + priv->adev = adev; + priv->platform_device = pdev; + + err = devm_mutex_init(&pdev->dev, &priv->vpc_mutex); + if (err) + return err; + + ideapad_check_features(priv); + + ideapad_debugfs_init(priv); + + err = ideapad_input_init(priv); + if (err) + goto input_failed; + + err = ideapad_kbd_bl_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up keyboard backlight LED: %d\n", err); + else + dev_info(&pdev->dev, "Keyboard backlight control not available\n"); + } + + err = ideapad_fn_lock_led_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up FnLock LED: %d\n", err); + else + dev_info(&pdev->dev, "FnLock control not available\n"); + } + + /* + * On some models without a hw-switch (the yoga 2 13 at least) + * VPCCMD_W_RF must be explicitly set to 1 for the wifi to work. + */ + if (!priv->features.hw_rfkill_switch) + write_ec_cmd(priv->adev->handle, VPCCMD_W_RF, 1); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + if (test_bit(ideapad_rfk_data[i].cfgbit, &priv->cfg)) + ideapad_register_rfkill(priv, i); + + ideapad_sync_rfk_state(priv); + ideapad_sync_touchpad_state(priv, false); + + err = ideapad_dytc_profile_init(priv); + if (err) { + if (err != -ENODEV) + dev_warn(&pdev->dev, "Could not set up DYTC interface: %d\n", err); + else + dev_info(&pdev->dev, "DYTC interface is not available\n"); + } + + if (acpi_video_get_backlight_type() == acpi_backlight_vendor) { + err = ideapad_backlight_init(priv); + if (err && err != -ENODEV) + goto backlight_failed; + } + + status = acpi_install_notify_handler(adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify, priv); + if (ACPI_FAILURE(status)) { + err = -EIO; + goto notification_failed; + } + + err = ideapad_shared_init(priv); + if (err) + goto shared_init_failed; + + ideapad_laptop_register_notifier(&ideapad_laptop_notifier); + + return 0; + +shared_init_failed: + acpi_remove_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify); + +notification_failed: + ideapad_backlight_exit(priv); + +backlight_failed: + ideapad_dytc_profile_exit(priv); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + ideapad_unregister_rfkill(priv, i); + + ideapad_fn_lock_led_exit(priv); + ideapad_kbd_bl_exit(priv); + ideapad_input_exit(priv); + +input_failed: + ideapad_debugfs_exit(priv); + + return err; +} + +static void ideapad_acpi_remove(struct platform_device *pdev) +{ + struct ideapad_private *priv = dev_get_drvdata(&pdev->dev); + int i; + + ideapad_laptop_unregister_notifier(&ideapad_laptop_notifier); + + ideapad_shared_exit(priv); + + acpi_remove_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, + ideapad_acpi_notify); + + ideapad_backlight_exit(priv); + ideapad_dytc_profile_exit(priv); + + for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) + ideapad_unregister_rfkill(priv, i); + + ideapad_fn_lock_led_exit(priv); + ideapad_kbd_bl_exit(priv); + ideapad_input_exit(priv); + ideapad_debugfs_exit(priv); +} + +#ifdef CONFIG_PM_SLEEP +static int ideapad_acpi_resume(struct device *dev) +{ + struct ideapad_private *priv = dev_get_drvdata(dev); + + ideapad_sync_rfk_state(priv); + ideapad_sync_touchpad_state(priv, false); + + if (priv->dytc) + dytc_profile_refresh(priv); + + return 0; +} +#endif +static SIMPLE_DEV_PM_OPS(ideapad_pm, NULL, ideapad_acpi_resume); + +static const struct acpi_device_id ideapad_device_ids[] = { + {"VPC2004", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, ideapad_device_ids); + +static struct platform_driver ideapad_acpi_driver = { + .probe = ideapad_acpi_add, + .remove = ideapad_acpi_remove, + .driver = { + .name = "ideapad_acpi", + .pm = &ideapad_pm, + .acpi_match_table = ACPI_PTR(ideapad_device_ids), + .dev_groups = ideapad_attribute_groups, + }, +}; + +static int __init ideapad_laptop_init(void) +{ + int err; + + err = ideapad_wmi_driver_register(); + if (err) + return err; + + err = platform_driver_register(&ideapad_acpi_driver); + if (err) { + ideapad_wmi_driver_unregister(); + return err; + } + + return 0; +} +module_init(ideapad_laptop_init) + +static void __exit ideapad_laptop_exit(void) +{ + ideapad_wmi_driver_unregister(); + platform_driver_unregister(&ideapad_acpi_driver); +} +module_exit(ideapad_laptop_exit) + +MODULE_AUTHOR("David Woodhouse "); +MODULE_DESCRIPTION("IdeaPad ACPI Extras"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.h b/drivers/platform/x86/lenovo/ideapad-laptop.h new file mode 100644 index 000000000000..1e52f2aa0aac --- /dev/null +++ b/drivers/platform/x86/lenovo/ideapad-laptop.h @@ -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 + */ + +#ifndef _IDEAPAD_LAPTOP_H_ +#define _IDEAPAD_LAPTOP_H_ + +#include + +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_ */ diff --git a/drivers/platform/x86/lenovo/think-lmi.c b/drivers/platform/x86/lenovo/think-lmi.c new file mode 100644 index 000000000000..34a47269e3d3 --- /dev/null +++ b/drivers/platform/x86/lenovo/think-lmi.c @@ -0,0 +1,1821 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Think LMI BIOS configuration driver + * + * Copyright(C) 2019-2021 Lenovo + * + * Original code from Thinkpad-wmi project https://github.com/iksaif/thinkpad-wmi + * Copyright(C) 2017 Corentin Chary + * Distributed under the GPL-2.0 license + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../firmware_attributes_class.h" +#include "think-lmi.h" + +static bool debug_support; +module_param(debug_support, bool, 0444); +MODULE_PARM_DESC(debug_support, "Enable debug command support"); + +/* + * Name: BiosSetting + * Description: Get item name and settings for current LMI instance. + * Type: Query + * Returns: "Item,Value" + * Example: "WakeOnLAN,Enable" + */ +#define LENOVO_BIOS_SETTING_GUID "51F5230E-9677-46CD-A1CF-C0B23EE34DB7" + +/* + * Name: SetBiosSetting + * Description: Change the BIOS setting to the desired value using the SetBiosSetting + * class. To save the settings, use the SaveBiosSetting class. + * BIOS settings and values are case sensitive. + * After making changes to the BIOS settings, you must reboot the computer + * before the changes will take effect. + * Type: Method + * Arguments: "Item,Value,Password,Encoding,KbdLang;" + * Example: "WakeOnLAN,Disable,pa55w0rd,ascii,us;" + */ +#define LENOVO_SET_BIOS_SETTINGS_GUID "98479A64-33F5-4E33-A707-8E251EBBC3A1" + +/* + * Name: SaveBiosSettings + * Description: Save any pending changes in settings. + * Type: Method + * Arguments: "Password,Encoding,KbdLang;" + * Example: "pa55w0rd,ascii,us;" + */ +#define LENOVO_SAVE_BIOS_SETTINGS_GUID "6A4B54EF-A5ED-4D33-9455-B0D9B48DF4B3" + +/* + * Name: BiosPasswordSettings + * Description: Return BIOS Password settings + * Type: Query + * Returns: PasswordMode, PasswordState, MinLength, MaxLength, + * SupportedEncoding, SupportedKeyboard + */ +#define LENOVO_BIOS_PASSWORD_SETTINGS_GUID "8ADB159E-1E32-455C-BC93-308A7ED98246" + +/* + * Name: SetBiosPassword + * Description: Change a specific password. + * - BIOS settings cannot be changed at the same boot as power-on + * passwords (POP) and hard disk passwords (HDP). If you want to change + * BIOS settings and POP or HDP, you must reboot the system after changing + * one of them. + * - A password cannot be set using this method when one does not already + * exist. Passwords can only be updated or cleared. + * Type: Method + * Arguments: "PasswordType,CurrentPassword,NewPassword,Encoding,KbdLang;" + * Example: "pop,pa55w0rd,newpa55w0rd,ascii,us;” + */ +#define LENOVO_SET_BIOS_PASSWORD_GUID "2651D9FD-911C-4B69-B94E-D0DED5963BD7" + +/* + * Name: GetBiosSelections + * Description: Return a list of valid settings for a given item. + * Type: Method + * Arguments: "Item" + * Returns: "Value1,Value2,Value3,..." + * Example: + * -> "FlashOverLAN" + * <- "Enabled,Disabled" + */ +#define LENOVO_GET_BIOS_SELECTIONS_GUID "7364651A-132F-4FE7-ADAA-40C6C7EE2E3B" + +/* + * Name: DebugCmd + * Description: Debug entry method for entering debug commands to the BIOS + */ +#define LENOVO_DEBUG_CMD_GUID "7FF47003-3B6C-4E5E-A227-E979824A85D1" + +/* + * Name: OpcodeIF + * Description: Opcode interface which provides the ability to set multiple + * parameters and then trigger an action with a final command. + * This is particularly useful for simplifying setting passwords. + * With this support comes the ability to set System, HDD and NVMe + * passwords. + * This is currently available on ThinkCenter and ThinkStations platforms + */ +#define LENOVO_OPCODE_IF_GUID "DFDDEF2C-57D4-48ce-B196-0FB787D90836" + +/* + * Name: SetBiosCert + * Description: Install BIOS certificate. + * Type: Method + * Arguments: "Certificate,Password" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_SET_BIOS_CERT_GUID "26861C9F-47E9-44C4-BD8B-DFE7FA2610FE" + +/* + * Name: UpdateBiosCert + * Description: Update BIOS certificate. + * Type: Method + * Format: "Certificate,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_UPDATE_BIOS_CERT_GUID "9AA3180A-9750-41F7-B9F7-D5D3B1BAC3CE" + +/* + * Name: ClearBiosCert + * Description: Uninstall BIOS certificate. + * Type: Method + * Format: "Serial,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_CLEAR_BIOS_CERT_GUID "B2BC39A7-78DD-4D71-B059-A510DEC44890" +/* + * Name: CertToPassword + * Description: Switch from certificate to password authentication. + * Type: Method + * Format: "Password,Signature" + * You must reboot the computer before the changes will take effect. + */ +#define LENOVO_CERT_TO_PASSWORD_GUID "0DE8590D-5510-4044-9621-77C227F5A70D" + +/* + * Name: SetBiosSettingCert + * Description: Set attribute using certificate authentication. + * Type: Method + * Format: "Item,Value,Signature" + */ +#define LENOVO_SET_BIOS_SETTING_CERT_GUID "34A008CC-D205-4B62-9E67-31DFA8B90003" + +/* + * Name: SaveBiosSettingCert + * Description: Save any pending changes in settings. + * Type: Method + * Format: "Signature" + */ +#define LENOVO_SAVE_BIOS_SETTING_CERT_GUID "C050FB9D-DF5F-4606-B066-9EFC401B2551" + +/* + * Name: CertThumbprint + * Description: Display Certificate thumbprints + * Type: Query + * Returns: MD5, SHA1 & SHA256 thumbprints + */ +#define LENOVO_CERT_THUMBPRINT_GUID "C59119ED-1C0D-4806-A8E9-59AA318176C4" + +#define TLMI_POP_PWD BIT(0) /* Supervisor */ +#define TLMI_PAP_PWD BIT(1) /* Power-on */ +#define TLMI_HDD_PWD BIT(2) /* HDD/NVME */ +#define TLMI_SMP_PWD BIT(6) /* System Management */ +#define TLMI_CERT_SVC BIT(7) /* Admin Certificate Based */ +#define TLMI_CERT_SMC BIT(8) /* System Certificate Based */ + +static const struct tlmi_err_codes tlmi_errs[] = { + {"Success", 0}, + {"Not Supported", -EOPNOTSUPP}, + {"Invalid Parameter", -EINVAL}, + {"Access Denied", -EACCES}, + {"System Busy", -EBUSY}, +}; + +static const char * const encoding_options[] = { + [TLMI_ENCODING_ASCII] = "ascii", + [TLMI_ENCODING_SCANCODE] = "scancode", +}; +static const char * const level_options[] = { + [TLMI_LEVEL_USER] = "user", + [TLMI_LEVEL_MASTER] = "master", +}; +static struct think_lmi tlmi_priv; +static DEFINE_MUTEX(tlmi_mutex); + +static inline struct tlmi_pwd_setting *to_tlmi_pwd_setting(struct kobject *kobj) +{ + return container_of(kobj, struct tlmi_pwd_setting, kobj); +} + +static inline struct tlmi_attr_setting *to_tlmi_attr_setting(struct kobject *kobj) +{ + return container_of(kobj, struct tlmi_attr_setting, kobj); +} + +/* Convert BIOS WMI error string to suitable error code */ +static int tlmi_errstr_to_err(const char *errstr) +{ + int i; + + for (i = 0; i < sizeof(tlmi_errs)/sizeof(struct tlmi_err_codes); i++) { + if (!strcmp(tlmi_errs[i].err_str, errstr)) + return tlmi_errs[i].err_code; + } + return -EPERM; +} + +/* Extract error string from WMI return buffer */ +static int tlmi_extract_error(const struct acpi_buffer *output) +{ + const union acpi_object *obj; + + obj = output->pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + return tlmi_errstr_to_err(obj->string.pointer); +} + +/* Utility function to execute WMI call to BIOS */ +static int tlmi_simple_call(const char *guid, const char *arg) +{ + const struct acpi_buffer input = { strlen(arg), (char *)arg }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + int i, err; + + /* + * Duplicated call required to match BIOS workaround for behavior + * seen when WMI accessed via scripting on other OS. + */ + for (i = 0; i < 2; i++) { + /* (re)initialize output buffer to default state */ + output.length = ACPI_ALLOCATE_BUFFER; + output.pointer = NULL; + + status = wmi_evaluate_method(guid, 0, 0, &input, &output); + if (ACPI_FAILURE(status)) { + kfree(output.pointer); + return -EIO; + } + err = tlmi_extract_error(&output); + kfree(output.pointer); + if (err) + return err; + } + return 0; +} + +/* Extract output string from WMI return value */ +static int tlmi_extract_output_string(union acpi_object *obj, char **string) +{ + char *s; + + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + s = kstrdup(obj->string.pointer, GFP_KERNEL); + if (!s) + return -ENOMEM; + *string = s; + return 0; +} + +/* ------ Core interface functions ------------*/ + +/* Get password settings from BIOS */ +static int tlmi_get_pwd_settings(struct tlmi_pwdcfg *pwdcfg) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + const union acpi_object *obj; + acpi_status status; + int copy_size; + + if (!tlmi_priv.can_get_password_settings) + return -EOPNOTSUPP; + + status = wmi_query_block(LENOVO_BIOS_PASSWORD_SETTINGS_GUID, 0, + &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_BUFFER || !obj->buffer.pointer) { + kfree(obj); + return -EIO; + } + /* + * The size of thinkpad_wmi_pcfg on ThinkStation is larger than ThinkPad. + * To make the driver compatible on different brands, we permit it to get + * the data in below case. + * Settings must have at minimum the core fields available + */ + if (obj->buffer.length < sizeof(struct tlmi_pwdcfg_core)) { + pr_warn("Unknown pwdcfg buffer length %d\n", obj->buffer.length); + kfree(obj); + return -EIO; + } + + copy_size = min_t(size_t, obj->buffer.length, sizeof(struct tlmi_pwdcfg)); + + memcpy(pwdcfg, obj->buffer.pointer, copy_size); + kfree(obj); + + if (WARN_ON(pwdcfg->core.max_length >= TLMI_PWD_BUFSIZE)) + pwdcfg->core.max_length = TLMI_PWD_BUFSIZE - 1; + return 0; +} + +static int tlmi_save_bios_settings(const char *password) +{ + return tlmi_simple_call(LENOVO_SAVE_BIOS_SETTINGS_GUID, + password); +} + +static int tlmi_opcode_setting(char *setting, const char *value) +{ + char *opcode_str; + int ret; + + opcode_str = kasprintf(GFP_KERNEL, "%s:%s;", setting, value); + if (!opcode_str) + return -ENOMEM; + + ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, opcode_str); + kfree(opcode_str); + return ret; +} + +static int tlmi_setting(struct wmi_device *wdev, int item, char **value) +{ + union acpi_object *obj; + int ret; + + obj = wmidev_block_query(wdev, item); + if (!obj) + return -EIO; + + ret = tlmi_extract_output_string(obj, value); + kfree(obj); + + return ret; +} + +static int tlmi_get_bios_selections(const char *item, char **value) +{ + const struct acpi_buffer input = { strlen(item), (char *)item }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int ret; + + status = wmi_evaluate_method(LENOVO_GET_BIOS_SELECTIONS_GUID, + 0, 0, &input, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + if (!obj) + return -ENODATA; + + ret = tlmi_extract_output_string(obj, value); + kfree(obj); + + return ret; +} + +/* ---- Authentication sysfs --------------------------------------------------------- */ +static ssize_t is_enabled_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->pwd_enabled || setting->cert_installed); +} + +static struct kobj_attribute auth_is_pass_set = __ATTR_RO(is_enabled); + +static ssize_t current_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + size_t pwdlen; + + pwdlen = strlen(buf); + /* pwdlen == 0 is allowed to clear the password */ + if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) + return -EINVAL; + + strscpy(setting->password, buf, setting->maxlen); + /* Strip out CR if one is present, setting password won't work if it is present */ + strreplace(setting->password, '\n', '\0'); + return count; +} + +static struct kobj_attribute auth_current_password = __ATTR_WO(current_password); + +static ssize_t new_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *auth_str, *new_pwd; + size_t pwdlen; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.can_set_bios_password) + return -EOPNOTSUPP; + + /* Strip out CR if one is present, setting password won't work if it is present */ + new_pwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_pwd) + return -ENOMEM; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + pwdlen = strlen(new_pwd); + /* pwdlen == 0 is allowed to clear the password */ + if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) { + ret = -EINVAL; + goto out; + } + + /* If opcode support is present use that interface */ + if (tlmi_priv.opcode_support) { + char pwd_type[8]; + + /* Special handling required for HDD and NVMe passwords */ + if (setting == tlmi_priv.pwd_hdd) { + if (setting->level == TLMI_LEVEL_USER) + sprintf(pwd_type, "uhdp%d", setting->index); + else + sprintf(pwd_type, "mhdp%d", setting->index); + } else if (setting == tlmi_priv.pwd_nvme) { + if (setting->level == TLMI_LEVEL_USER) + sprintf(pwd_type, "udrp%d", setting->index); + else + sprintf(pwd_type, "adrp%d", setting->index); + } else { + sprintf(pwd_type, "%s", setting->pwd_type); + } + + ret = tlmi_opcode_setting("WmiOpcodePasswordType", pwd_type); + if (ret) + goto out; + + /* + * Note admin password is not always required if SMPControl enabled in BIOS, + * So only set if it's configured. + * Let BIOS figure it out - we'll get an error if operation is not permitted + */ + if (tlmi_priv.pwd_admin->pwd_enabled && strlen(tlmi_priv.pwd_admin->password)) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + ret = tlmi_opcode_setting("WmiOpcodePasswordCurrent01", setting->password); + if (ret) + goto out; + ret = tlmi_opcode_setting("WmiOpcodePasswordNew01", new_pwd); + if (ret) + goto out; + ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, "WmiOpcodePasswordSetUpdate;"); + } else { + /* Format: 'PasswordType,CurrentPw,NewPw,Encoding,KbdLang;' */ + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s,%s,%s;", + setting->pwd_type, setting->password, new_pwd, + encoding_options[setting->encoding], setting->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + ret = tlmi_simple_call(LENOVO_SET_BIOS_PASSWORD_GUID, auth_str); + kfree(auth_str); + } +out: + mutex_unlock(&tlmi_mutex); + kfree(new_pwd); + return ret ?: count; +} + +static struct kobj_attribute auth_new_password = __ATTR_WO(new_password); + +static ssize_t min_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->minlen); +} + +static struct kobj_attribute auth_min_pass_length = __ATTR_RO(min_password_length); + +static ssize_t max_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->maxlen); +} +static struct kobj_attribute auth_max_pass_length = __ATTR_RO(max_password_length); + +static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + if (setting->cert_installed) + return sysfs_emit(buf, "certificate\n"); + return sysfs_emit(buf, "password\n"); +} +static struct kobj_attribute auth_mechanism = __ATTR_RO(mechanism); + +static ssize_t encoding_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", encoding_options[setting->encoding]); +} + +static ssize_t encoding_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int i; + + /* Scan for a matching profile */ + i = sysfs_match_string(encoding_options, buf); + if (i < 0) + return -EINVAL; + + setting->encoding = i; + return count; +} + +static struct kobj_attribute auth_encoding = __ATTR_RW(encoding); + +static ssize_t kbdlang_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->kbdlang); +} + +static ssize_t kbdlang_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int length; + + /* Calculate length till '\n' or terminating 0 */ + length = strchrnul(buf, '\n') - buf; + if (!length || length >= TLMI_LANG_MAXLEN) + return -EINVAL; + + memcpy(setting->kbdlang, buf, length); + setting->kbdlang[length] = '\0'; + return count; +} + +static struct kobj_attribute auth_kbdlang = __ATTR_RW(kbdlang); + +static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->role); +} +static struct kobj_attribute auth_role = __ATTR_RO(role); + +static ssize_t index_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%d\n", setting->index); +} + +static ssize_t index_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int err, val; + + err = kstrtoint(buf, 10, &val); + if (err < 0) + return err; + + if (val < 0 || val > TLMI_INDEX_MAX) + return -EINVAL; + + setting->index = val; + return count; +} + +static struct kobj_attribute auth_index = __ATTR_RW(index); + +static ssize_t level_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + return sysfs_emit(buf, "%s\n", level_options[setting->level]); +} + +static ssize_t level_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + int i; + + /* Scan for a matching profile */ + i = sysfs_match_string(level_options, buf); + if (i < 0) + return -EINVAL; + + setting->level = i; + return count; +} + +static struct kobj_attribute auth_level = __ATTR_RW(level); + +static char *cert_command(struct tlmi_pwd_setting *setting, const char *arg1, const char *arg2) +{ + /* Prepend with SVC or SMC if multicert supported */ + if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) + return kasprintf(GFP_KERNEL, "%s,%s,%s", + setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", + arg1, arg2); + else + return kasprintf(GFP_KERNEL, "%s,%s", arg1, arg2); +} + +static ssize_t cert_thumbprint(char *buf, const char *arg, int count) +{ + const struct acpi_buffer input = { strlen(arg), (char *)arg }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + const union acpi_object *obj; + acpi_status status; + + status = wmi_evaluate_method(LENOVO_CERT_THUMBPRINT_GUID, 0, 0, &input, &output); + if (ACPI_FAILURE(status)) { + kfree(output.pointer); + return -EIO; + } + obj = output.pointer; + if (!obj) + return -ENOMEM; + if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) { + kfree(output.pointer); + return -EIO; + } + count += sysfs_emit_at(buf, count, "%s : %s\n", arg, (char *)obj->string.pointer); + kfree(output.pointer); + + return count; +} + +static char *thumbtypes[] = {"Md5", "Sha1", "Sha256"}; + +static ssize_t certificate_thumbprint_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + unsigned int i; + int count = 0; + char *wmistr; + + if (!tlmi_priv.certificate_support || !setting->cert_installed) + return -EOPNOTSUPP; + + for (i = 0; i < ARRAY_SIZE(thumbtypes); i++) { + if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) { + /* Format: 'SVC | SMC, Thumbtype' */ + wmistr = kasprintf(GFP_KERNEL, "%s,%s", + setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", + thumbtypes[i]); + } else { + /* Format: 'Thumbtype' */ + wmistr = kasprintf(GFP_KERNEL, "%s", thumbtypes[i]); + } + if (!wmistr) + return -ENOMEM; + count += cert_thumbprint(buf, wmistr, count); + kfree(wmistr); + } + + return count; +} + +static struct kobj_attribute auth_cert_thumb = __ATTR_RO(certificate_thumbprint); + +static ssize_t cert_to_password_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *auth_str, *passwd; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + if (!setting->cert_installed) + return -EINVAL; + + if (!setting->signature || !setting->signature[0]) + return -EACCES; + + /* Strip out CR if one is present */ + passwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!passwd) + return -ENOMEM; + + /* Format: 'Password,Signature' */ + auth_str = cert_command(setting, passwd, setting->signature); + if (!auth_str) { + kfree_sensitive(passwd); + return -ENOMEM; + } + ret = tlmi_simple_call(LENOVO_CERT_TO_PASSWORD_GUID, auth_str); + kfree(auth_str); + kfree_sensitive(passwd); + + return ret ?: count; +} + +static struct kobj_attribute auth_cert_to_password = __ATTR_WO(cert_to_password); + +enum cert_install_mode { + TLMI_CERT_INSTALL, + TLMI_CERT_UPDATE, +}; + +static ssize_t certificate_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + enum cert_install_mode install_mode = TLMI_CERT_INSTALL; + char *auth_str, *new_cert; + const char *serial; + char *signature; + char *guid; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* If empty then clear installed certificate */ + if ((buf[0] == '\0') || (buf[0] == '\n')) { /* Clear installed certificate */ + /* Check that signature is set */ + if (!setting->signature || !setting->signature[0]) + return -EACCES; + + /* Format: 'serial#, signature' */ + serial = dmi_get_system_info(DMI_PRODUCT_SERIAL); + if (!serial) + return -ENODEV; + auth_str = cert_command(setting, serial, setting->signature); + if (!auth_str) + return -ENOMEM; + + ret = tlmi_simple_call(LENOVO_CLEAR_BIOS_CERT_GUID, auth_str); + kfree(auth_str); + + return ret ?: count; + } + + /* Strip out CR if one is present */ + new_cert = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_cert) + return -ENOMEM; + + if (setting->cert_installed) { + /* Certificate is installed so this is an update */ + install_mode = TLMI_CERT_UPDATE; + /* If admin account enabled - need to use its signature */ + if (tlmi_priv.pwd_admin->pwd_enabled) + signature = tlmi_priv.pwd_admin->signature; + else + signature = setting->signature; + } else { /* Cert install */ + /* Check if SMC and SVC already installed */ + if ((setting == tlmi_priv.pwd_system) && tlmi_priv.pwd_admin->cert_installed) { + /* This gets treated as a cert update */ + install_mode = TLMI_CERT_UPDATE; + signature = tlmi_priv.pwd_admin->signature; + } else { /* Regular cert install */ + install_mode = TLMI_CERT_INSTALL; + signature = setting->signature; + } + } + + if (install_mode == TLMI_CERT_UPDATE) { + /* This is a certificate update */ + if (!signature || !signature[0]) { + kfree(new_cert); + return -EACCES; + } + guid = LENOVO_UPDATE_BIOS_CERT_GUID; + /* Format: 'Certificate,Signature' */ + auth_str = cert_command(setting, new_cert, signature); + } else { + /* This is a fresh install */ + /* To set admin cert, a password must be enabled */ + if ((setting == tlmi_priv.pwd_admin) && + (!setting->pwd_enabled || !setting->password[0])) { + kfree(new_cert); + return -EACCES; + } + guid = LENOVO_SET_BIOS_CERT_GUID; + /* Format: 'Certificate, password' */ + auth_str = cert_command(setting, new_cert, setting->password); + } + kfree(new_cert); + if (!auth_str) + return -ENOMEM; + + ret = tlmi_simple_call(guid, auth_str); + kfree(auth_str); + + return ret ?: count; +} + +static struct kobj_attribute auth_certificate = __ATTR_WO(certificate); + +static ssize_t signature_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *new_signature; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_signature) + return -ENOMEM; + + /* Free any previous signature */ + kfree(setting->signature); + setting->signature = new_signature; + + return count; +} + +static struct kobj_attribute auth_signature = __ATTR_WO(signature); + +static ssize_t save_signature_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + char *new_signature; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (!tlmi_priv.certificate_support) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_signature) + return -ENOMEM; + + /* Free any previous signature */ + kfree(setting->save_signature); + setting->save_signature = new_signature; + + return count; +} + +static struct kobj_attribute auth_save_signature = __ATTR_WO(save_signature); + +static umode_t auth_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + /* We only want to display level and index settings on HDD/NVMe */ + if (attr == &auth_index.attr || attr == &auth_level.attr) { + if ((setting == tlmi_priv.pwd_hdd) || (setting == tlmi_priv.pwd_nvme)) + return attr->mode; + return 0; + } + + /* We only display certificates, if supported */ + if (attr == &auth_certificate.attr || + attr == &auth_signature.attr || + attr == &auth_save_signature.attr || + attr == &auth_cert_thumb.attr || + attr == &auth_cert_to_password.attr) { + if (tlmi_priv.certificate_support) { + if (setting == tlmi_priv.pwd_admin) + return attr->mode; + if ((tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) && + (setting == tlmi_priv.pwd_system)) + return attr->mode; + } + return 0; + } + + /* Don't display un-needed settings if opcode available */ + if ((attr == &auth_encoding.attr || attr == &auth_kbdlang.attr) && + tlmi_priv.opcode_support) + return 0; + + return attr->mode; +} + +static struct attribute *auth_attrs[] = { + &auth_is_pass_set.attr, + &auth_min_pass_length.attr, + &auth_max_pass_length.attr, + &auth_current_password.attr, + &auth_new_password.attr, + &auth_role.attr, + &auth_mechanism.attr, + &auth_encoding.attr, + &auth_kbdlang.attr, + &auth_index.attr, + &auth_level.attr, + &auth_certificate.attr, + &auth_signature.attr, + &auth_save_signature.attr, + &auth_cert_thumb.attr, + &auth_cert_to_password.attr, + NULL +}; + +static const struct attribute_group auth_attr_group = { + .is_visible = auth_attr_is_visible, + .attrs = auth_attrs, +}; + +/* ---- Attributes sysfs --------------------------------------------------------- */ +static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->display_name); +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + char *item, *value; + int ret; + + ret = tlmi_setting(setting->wdev, setting->index, &item); + if (ret) + return ret; + + /* validate and split from `item,value` -> `value` */ + value = strpbrk(item, ","); + if (!value || value == item || !strlen(value + 1)) + ret = -EINVAL; + else { + /* On Workstations remove the Options part after the value */ + strreplace(value, ';', '\0'); + ret = sysfs_emit(buf, "%s\n", value + 1); + } + kfree(item); + + return ret; +} + +static ssize_t possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + return sysfs_emit(buf, "%s\n", setting->possible_values); +} + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + if (setting->possible_values) { + /* Figure out what setting type is as BIOS does not return this */ + if (strchr(setting->possible_values, ';')) + return sysfs_emit(buf, "enumeration\n"); + } + /* Anything else is going to be a string */ + return sysfs_emit(buf, "string\n"); +} + +static ssize_t current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + char *set_str = NULL, *new_setting = NULL; + char *auth_str = NULL; + int ret; + + if (!tlmi_priv.can_set_bios_settings) + return -EOPNOTSUPP; + + /* + * If we are using bulk saves a reboot should be done once save has + * been called + */ + if (tlmi_priv.save_mode == TLMI_SAVE_BULK && tlmi_priv.reboot_required) + return -EPERM; + + /* Strip out CR if one is present */ + new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_setting) + return -ENOMEM; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + /* Check if certificate authentication is enabled and active */ + if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { + if (!tlmi_priv.pwd_admin->signature || !tlmi_priv.pwd_admin->save_signature) { + ret = -EINVAL; + goto out; + } + set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, + new_setting, tlmi_priv.pwd_admin->signature); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTING_CERT_GUID, set_str); + if (ret) + goto out; + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) + tlmi_priv.save_required = true; + else + ret = tlmi_simple_call(LENOVO_SAVE_BIOS_SETTING_CERT_GUID, + tlmi_priv.pwd_admin->save_signature); + } else if (tlmi_priv.opcode_support) { + /* + * If opcode support is present use that interface. + * Note - this sets the variable and then the password as separate + * WMI calls. Function tlmi_save_bios_settings will error if the + * password is incorrect. + * Workstation's require the opcode to be set before changing the + * attribute. + */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + + set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, + new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); + if (ret) + goto out; + + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) + tlmi_priv.save_required = true; + else + ret = tlmi_save_bios_settings(""); + } else { /* old non-opcode based authentication method (deprecated) */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, + new_setting, auth_str); + else + set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, + new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); + if (ret) + goto out; + + if (tlmi_priv.save_mode == TLMI_SAVE_BULK) { + tlmi_priv.save_required = true; + } else { + if (auth_str) + ret = tlmi_save_bios_settings(auth_str); + else + ret = tlmi_save_bios_settings(""); + } + } + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } +out: + mutex_unlock(&tlmi_mutex); + kfree(auth_str); + kfree(set_str); + kfree(new_setting); + return ret ?: count; +} + +static struct kobj_attribute attr_displ_name = __ATTR_RO(display_name); + +static struct kobj_attribute attr_possible_values = __ATTR_RO(possible_values); + +static struct kobj_attribute attr_current_val = __ATTR_RW_MODE(current_value, 0600); + +static struct kobj_attribute attr_type = __ATTR_RO(type); + +static umode_t attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + /* We don't want to display possible_values attributes if not available */ + if ((attr == &attr_possible_values.attr) && (!setting->possible_values)) + return 0; + + return attr->mode; +} + +static struct attribute *tlmi_attrs[] = { + &attr_displ_name.attr, + &attr_current_val.attr, + &attr_possible_values.attr, + &attr_type.attr, + NULL +}; + +static const struct attribute_group tlmi_attr_group = { + .is_visible = attr_is_visible, + .attrs = tlmi_attrs, +}; + +static void tlmi_attr_setting_release(struct kobject *kobj) +{ + struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); + + kfree(setting->possible_values); + kfree(setting); +} + +static void tlmi_pwd_setting_release(struct kobject *kobj) +{ + struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); + + kfree(setting); +} + +static const struct kobj_type tlmi_attr_setting_ktype = { + .release = &tlmi_attr_setting_release, + .sysfs_ops = &kobj_sysfs_ops, +}; + +static const struct kobj_type tlmi_pwd_setting_ktype = { + .release = &tlmi_pwd_setting_release, + .sysfs_ops = &kobj_sysfs_ops, +}; + +static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sprintf(buf, "%d\n", tlmi_priv.pending_changes); +} + +static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); + +static const char * const save_mode_strings[] = { + [TLMI_SAVE_SINGLE] = "single", + [TLMI_SAVE_BULK] = "bulk", + [TLMI_SAVE_SAVE] = "save" +}; + +static ssize_t save_settings_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + /* Check that setting is valid */ + if (WARN_ON(tlmi_priv.save_mode < TLMI_SAVE_SINGLE || + tlmi_priv.save_mode > TLMI_SAVE_BULK)) + return -EIO; + return sysfs_emit(buf, "%s\n", save_mode_strings[tlmi_priv.save_mode]); +} + +static ssize_t save_settings_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *auth_str = NULL; + int ret = 0; + int cmd; + + cmd = sysfs_match_string(save_mode_strings, buf); + if (cmd < 0) + return cmd; + + /* Use lock in case multiple WMI operations needed */ + mutex_lock(&tlmi_mutex); + + switch (cmd) { + case TLMI_SAVE_SINGLE: + case TLMI_SAVE_BULK: + tlmi_priv.save_mode = cmd; + goto out; + case TLMI_SAVE_SAVE: + /* Check if supported*/ + if (!tlmi_priv.can_set_bios_settings || + tlmi_priv.save_mode == TLMI_SAVE_SINGLE) { + ret = -EOPNOTSUPP; + goto out; + } + /* Check there is actually something to save */ + if (!tlmi_priv.save_required) { + ret = -ENOENT; + goto out; + } + /* Check if certificate authentication is enabled and active */ + if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { + if (!tlmi_priv.pwd_admin->signature || + !tlmi_priv.pwd_admin->save_signature) { + ret = -EINVAL; + goto out; + } + ret = tlmi_simple_call(LENOVO_SAVE_BIOS_SETTING_CERT_GUID, + tlmi_priv.pwd_admin->save_signature); + if (ret) + goto out; + } else if (tlmi_priv.opcode_support) { + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", + tlmi_priv.pwd_admin->password); + if (ret) + goto out; + } + ret = tlmi_save_bios_settings(""); + } else { /* old non-opcode based authentication method (deprecated) */ + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + ret = tlmi_save_bios_settings(auth_str); + else + ret = tlmi_save_bios_settings(""); + } + tlmi_priv.save_required = false; + tlmi_priv.reboot_required = true; + + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } + break; + } +out: + mutex_unlock(&tlmi_mutex); + kfree(auth_str); + return ret ?: count; +} + +static struct kobj_attribute save_settings = __ATTR_RW(save_settings); + +/* ---- Debug interface--------------------------------------------------------- */ +static ssize_t debug_cmd_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + char *set_str = NULL, *new_setting = NULL; + char *auth_str = NULL; + int ret; + + if (!tlmi_priv.can_debug_cmd) + return -EOPNOTSUPP; + + /* Strip out CR if one is present */ + new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); + if (!new_setting) + return -ENOMEM; + + if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { + auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", + tlmi_priv.pwd_admin->password, + encoding_options[tlmi_priv.pwd_admin->encoding], + tlmi_priv.pwd_admin->kbdlang); + if (!auth_str) { + ret = -ENOMEM; + goto out; + } + } + + if (auth_str) + set_str = kasprintf(GFP_KERNEL, "%s,%s", new_setting, auth_str); + else + set_str = kasprintf(GFP_KERNEL, "%s;", new_setting); + if (!set_str) { + ret = -ENOMEM; + goto out; + } + + ret = tlmi_simple_call(LENOVO_DEBUG_CMD_GUID, set_str); + if (ret) + goto out; + + if (!ret && !tlmi_priv.pending_changes) { + tlmi_priv.pending_changes = true; + /* let userland know it may need to check reboot pending again */ + kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); + } +out: + kfree(auth_str); + kfree(set_str); + kfree(new_setting); + return ret ?: count; +} + +static struct kobj_attribute debug_cmd = __ATTR_WO(debug_cmd); + +/* ---- Initialisation --------------------------------------------------------- */ +static void tlmi_release_attr(void) +{ + int i; + + /* Attribute structures */ + for (i = 0; i < TLMI_SETTINGS_COUNT; i++) { + if (tlmi_priv.setting[i]) { + sysfs_remove_group(&tlmi_priv.setting[i]->kobj, &tlmi_attr_group); + kobject_put(&tlmi_priv.setting[i]->kobj); + } + } + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); + + if (tlmi_priv.can_debug_cmd && debug_support) + sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); + + kset_unregister(tlmi_priv.attribute_kset); + + /* Free up any saved signatures */ + kfree(tlmi_priv.pwd_admin->signature); + kfree(tlmi_priv.pwd_admin->save_signature); + + /* Authentication structures */ + sysfs_remove_group(&tlmi_priv.pwd_admin->kobj, &auth_attr_group); + kobject_put(&tlmi_priv.pwd_admin->kobj); + sysfs_remove_group(&tlmi_priv.pwd_power->kobj, &auth_attr_group); + kobject_put(&tlmi_priv.pwd_power->kobj); + + if (tlmi_priv.opcode_support) { + sysfs_remove_group(&tlmi_priv.pwd_system->kobj, &auth_attr_group); + kobject_put(&tlmi_priv.pwd_system->kobj); + sysfs_remove_group(&tlmi_priv.pwd_hdd->kobj, &auth_attr_group); + kobject_put(&tlmi_priv.pwd_hdd->kobj); + sysfs_remove_group(&tlmi_priv.pwd_nvme->kobj, &auth_attr_group); + kobject_put(&tlmi_priv.pwd_nvme->kobj); + } + + kset_unregister(tlmi_priv.authentication_kset); +} + +static int tlmi_validate_setting_name(struct kset *attribute_kset, char *name) +{ + struct kobject *duplicate; + + if (!strcmp(name, "Reserved")) + return -EINVAL; + + duplicate = kset_find_obj(attribute_kset, name); + if (duplicate) { + pr_debug("Duplicate attribute name found - %s\n", name); + /* kset_find_obj() returns a reference */ + kobject_put(duplicate); + return -EBUSY; + } + + return 0; +} + +static int tlmi_sysfs_init(void) +{ + int i, ret; + + tlmi_priv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", "thinklmi"); + if (IS_ERR(tlmi_priv.class_dev)) { + ret = PTR_ERR(tlmi_priv.class_dev); + goto fail_class_created; + } + + tlmi_priv.attribute_kset = kset_create_and_add("attributes", NULL, + &tlmi_priv.class_dev->kobj); + if (!tlmi_priv.attribute_kset) { + ret = -ENOMEM; + goto fail_device_created; + } + + for (i = 0; i < TLMI_SETTINGS_COUNT; i++) { + /* Check if index is a valid setting - skip if it isn't */ + if (!tlmi_priv.setting[i]) + continue; + + /* check for duplicate or reserved values */ + if (tlmi_validate_setting_name(tlmi_priv.attribute_kset, + tlmi_priv.setting[i]->display_name) < 0) { + kfree(tlmi_priv.setting[i]->possible_values); + kfree(tlmi_priv.setting[i]); + tlmi_priv.setting[i] = NULL; + continue; + } + + /* Build attribute */ + tlmi_priv.setting[i]->kobj.kset = tlmi_priv.attribute_kset; + ret = kobject_add(&tlmi_priv.setting[i]->kobj, NULL, + "%s", tlmi_priv.setting[i]->display_name); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.setting[i]->kobj, &tlmi_attr_group); + if (ret) + goto fail_create_attr; + } + + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); + if (ret) + goto fail_create_attr; + + if (tlmi_priv.can_debug_cmd && debug_support) { + ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); + if (ret) + goto fail_create_attr; + } + + /* Create authentication entries */ + tlmi_priv.authentication_kset = kset_create_and_add("authentication", NULL, + &tlmi_priv.class_dev->kobj); + if (!tlmi_priv.authentication_kset) { + ret = -ENOMEM; + goto fail_create_attr; + } + tlmi_priv.pwd_admin->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_add(&tlmi_priv.pwd_admin->kobj, NULL, "%s", "Admin"); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.pwd_admin->kobj, &auth_attr_group); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_power->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_add(&tlmi_priv.pwd_power->kobj, NULL, "%s", "Power-on"); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.pwd_power->kobj, &auth_attr_group); + if (ret) + goto fail_create_attr; + + if (tlmi_priv.opcode_support) { + tlmi_priv.pwd_system->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_add(&tlmi_priv.pwd_system->kobj, NULL, "%s", "System"); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.pwd_system->kobj, &auth_attr_group); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_hdd->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_add(&tlmi_priv.pwd_hdd->kobj, NULL, "%s", "HDD"); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.pwd_hdd->kobj, &auth_attr_group); + if (ret) + goto fail_create_attr; + + tlmi_priv.pwd_nvme->kobj.kset = tlmi_priv.authentication_kset; + ret = kobject_add(&tlmi_priv.pwd_nvme->kobj, NULL, "%s", "NVMe"); + if (ret) + goto fail_create_attr; + + ret = sysfs_create_group(&tlmi_priv.pwd_nvme->kobj, &auth_attr_group); + if (ret) + goto fail_create_attr; + } + + return ret; + +fail_create_attr: + tlmi_release_attr(); +fail_device_created: + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); +fail_class_created: + return ret; +} + +/* ---- Base Driver -------------------------------------------------------- */ +static struct tlmi_pwd_setting *tlmi_create_auth(const char *pwd_type, + const char *pwd_role) +{ + struct tlmi_pwd_setting *new_pwd; + + new_pwd = kzalloc(sizeof(struct tlmi_pwd_setting), GFP_KERNEL); + if (!new_pwd) + return NULL; + + strscpy(new_pwd->kbdlang, "us"); + new_pwd->encoding = TLMI_ENCODING_ASCII; + new_pwd->pwd_type = pwd_type; + new_pwd->role = pwd_role; + new_pwd->minlen = tlmi_priv.pwdcfg.core.min_length; + new_pwd->maxlen = tlmi_priv.pwdcfg.core.max_length; + new_pwd->index = 0; + + kobject_init(&new_pwd->kobj, &tlmi_pwd_setting_ktype); + + return new_pwd; +} + +static int tlmi_analyze(struct wmi_device *wdev) +{ + int i, ret; + + if (wmi_has_guid(LENOVO_SET_BIOS_SETTINGS_GUID) && + wmi_has_guid(LENOVO_SAVE_BIOS_SETTINGS_GUID)) + tlmi_priv.can_set_bios_settings = true; + + if (wmi_has_guid(LENOVO_GET_BIOS_SELECTIONS_GUID)) + tlmi_priv.can_get_bios_selections = true; + + if (wmi_has_guid(LENOVO_SET_BIOS_PASSWORD_GUID)) + tlmi_priv.can_set_bios_password = true; + + if (wmi_has_guid(LENOVO_BIOS_PASSWORD_SETTINGS_GUID)) + tlmi_priv.can_get_password_settings = true; + + if (wmi_has_guid(LENOVO_DEBUG_CMD_GUID)) + tlmi_priv.can_debug_cmd = true; + + if (wmi_has_guid(LENOVO_OPCODE_IF_GUID)) + tlmi_priv.opcode_support = true; + + if (wmi_has_guid(LENOVO_SET_BIOS_CERT_GUID) && + wmi_has_guid(LENOVO_SET_BIOS_SETTING_CERT_GUID) && + wmi_has_guid(LENOVO_SAVE_BIOS_SETTING_CERT_GUID)) + tlmi_priv.certificate_support = true; + + /* + * Try to find the number of valid settings of this machine + * and use it to create sysfs attributes. + */ + for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { + struct tlmi_attr_setting *setting; + char *item = NULL; + + tlmi_priv.setting[i] = NULL; + ret = tlmi_setting(wdev, i, &item); + if (ret) + break; + if (!item) + break; + if (!*item) { + kfree(item); + continue; + } + + /* Remove the value part */ + strreplace(item, ',', '\0'); + + /* Create a setting entry */ + setting = kzalloc(sizeof(*setting), GFP_KERNEL); + if (!setting) { + ret = -ENOMEM; + kfree(item); + goto fail_clear_attr; + } + setting->wdev = wdev; + setting->index = i; + + strscpy(setting->name, item); + /* It is not allowed to have '/' for file name. Convert it into '\'. */ + strreplace(item, '/', '\\'); + strscpy(setting->display_name, item); + + /* If BIOS selections supported, load those */ + if (tlmi_priv.can_get_bios_selections) { + ret = tlmi_get_bios_selections(setting->name, + &setting->possible_values); + if (ret || !setting->possible_values) + pr_info("Error retrieving possible values for %d : %s\n", + i, setting->display_name); + } else { + /* + * Older Thinkstations don't support the bios_selections API. + * Instead they store this as a [Optional:Option1,Option2] section of the + * name string. + * Try and pull that out if it's available. + */ + char *optitem, *optstart, *optend; + + if (!tlmi_setting(setting->wdev, setting->index, &optitem)) { + optstart = strstr(optitem, "[Optional:"); + if (optstart) { + optstart += strlen("[Optional:"); + optend = strstr(optstart, "]"); + if (optend) + setting->possible_values = + kstrndup(optstart, optend - optstart, + GFP_KERNEL); + } + kfree(optitem); + } + } + /* + * firmware-attributes requires that possible_values are separated by ';' but + * Lenovo FW uses ','. Replace appropriately. + */ + if (setting->possible_values) + strreplace(setting->possible_values, ',', ';'); + + kobject_init(&setting->kobj, &tlmi_attr_setting_ktype); + tlmi_priv.setting[i] = setting; + kfree(item); + } + + /* Create password setting structure */ + ret = tlmi_get_pwd_settings(&tlmi_priv.pwdcfg); + if (ret) + goto fail_clear_attr; + + /* All failures below boil down to kmalloc failures */ + ret = -ENOMEM; + + tlmi_priv.pwd_admin = tlmi_create_auth("pap", "bios-admin"); + if (!tlmi_priv.pwd_admin) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_PAP_PWD) + tlmi_priv.pwd_admin->pwd_enabled = true; + + tlmi_priv.pwd_power = tlmi_create_auth("pop", "power-on"); + if (!tlmi_priv.pwd_power) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_POP_PWD) + tlmi_priv.pwd_power->pwd_enabled = true; + + if (tlmi_priv.opcode_support) { + tlmi_priv.pwd_system = tlmi_create_auth("smp", "system"); + if (!tlmi_priv.pwd_system) + goto fail_clear_attr; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_SMP_PWD) + tlmi_priv.pwd_system->pwd_enabled = true; + + tlmi_priv.pwd_hdd = tlmi_create_auth("hdd", "hdd"); + if (!tlmi_priv.pwd_hdd) + goto fail_clear_attr; + + tlmi_priv.pwd_nvme = tlmi_create_auth("nvm", "nvme"); + if (!tlmi_priv.pwd_nvme) + goto fail_clear_attr; + + /* Set default hdd/nvme index to 1 as there is no device 0 */ + tlmi_priv.pwd_hdd->index = 1; + tlmi_priv.pwd_nvme->index = 1; + + if (tlmi_priv.pwdcfg.core.password_state & TLMI_HDD_PWD) { + /* Check if PWD is configured and set index to first drive found */ + if (tlmi_priv.pwdcfg.ext.hdd_user_password || + tlmi_priv.pwdcfg.ext.hdd_master_password) { + tlmi_priv.pwd_hdd->pwd_enabled = true; + if (tlmi_priv.pwdcfg.ext.hdd_master_password) + tlmi_priv.pwd_hdd->index = + ffs(tlmi_priv.pwdcfg.ext.hdd_master_password) - 1; + else + tlmi_priv.pwd_hdd->index = + ffs(tlmi_priv.pwdcfg.ext.hdd_user_password) - 1; + } + if (tlmi_priv.pwdcfg.ext.nvme_user_password || + tlmi_priv.pwdcfg.ext.nvme_master_password) { + tlmi_priv.pwd_nvme->pwd_enabled = true; + if (tlmi_priv.pwdcfg.ext.nvme_master_password) + tlmi_priv.pwd_nvme->index = + ffs(tlmi_priv.pwdcfg.ext.nvme_master_password) - 1; + else + tlmi_priv.pwd_nvme->index = + ffs(tlmi_priv.pwdcfg.ext.nvme_user_password) - 1; + } + } + } + + if (tlmi_priv.certificate_support) { + tlmi_priv.pwd_admin->cert_installed = + tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SVC; + tlmi_priv.pwd_system->cert_installed = + tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SMC; + } + return 0; + +fail_clear_attr: + for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { + if (tlmi_priv.setting[i]) { + kfree(tlmi_priv.setting[i]->possible_values); + kfree(tlmi_priv.setting[i]); + } + } + kfree(tlmi_priv.pwd_admin); + kfree(tlmi_priv.pwd_power); + kfree(tlmi_priv.pwd_system); + kfree(tlmi_priv.pwd_hdd); + kfree(tlmi_priv.pwd_nvme); + return ret; +} + +static void tlmi_remove(struct wmi_device *wdev) +{ + tlmi_release_attr(); + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); +} + +static int tlmi_probe(struct wmi_device *wdev, const void *context) +{ + int ret; + + ret = tlmi_analyze(wdev); + if (ret) + return ret; + + return tlmi_sysfs_init(); +} + +static const struct wmi_device_id tlmi_id_table[] = { + { .guid_string = LENOVO_BIOS_SETTING_GUID }, + { } +}; +MODULE_DEVICE_TABLE(wmi, tlmi_id_table); + +static struct wmi_driver tlmi_driver = { + .driver = { + .name = "think-lmi", + }, + .id_table = tlmi_id_table, + .probe = tlmi_probe, + .remove = tlmi_remove, +}; + +MODULE_AUTHOR("Sugumaran L "); +MODULE_AUTHOR("Mark Pearson "); +MODULE_AUTHOR("Corentin Chary "); +MODULE_DESCRIPTION("ThinkLMI Driver"); +MODULE_LICENSE("GPL"); + +module_wmi_driver(tlmi_driver); diff --git a/drivers/platform/x86/lenovo/think-lmi.h b/drivers/platform/x86/lenovo/think-lmi.h new file mode 100644 index 000000000000..9b014644d316 --- /dev/null +++ b/drivers/platform/x86/lenovo/think-lmi.h @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef _THINK_LMI_H_ +#define _THINK_LMI_H_ + +#include +#include + +#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, +}; + +/* 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; + + 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; +}; + +#endif /* !_THINK_LMI_H_ */ diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c new file mode 100644 index 000000000000..e1c7bd06fa12 --- /dev/null +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c @@ -0,0 +1,12096 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * thinkpad_acpi.c - ThinkPad ACPI Extras + * + * Copyright (C) 2004-2005 Borislav Deianov + * Copyright (C) 2006-2009 Henrique de Moraes Holschuh + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#define TPACPI_VERSION "0.26" +#define TPACPI_SYSFS_VERSION 0x030000 + +/* + * Changelog: + * 2007-10-20 changelog trimmed down + * + * 2007-03-27 0.14 renamed to thinkpad_acpi and moved to + * drivers/misc. + * + * 2006-11-22 0.13 new maintainer + * changelog now lives in git commit history, and will + * not be updated further in-file. + * + * 2005-03-17 0.11 support for 600e, 770x + * thanks to Jamie Lentin + * + * 2005-01-16 0.9 use MODULE_VERSION + * thanks to Henrik Brix Andersen + * fix parameter passing on module loading + * thanks to Rusty Russell + * thanks to Jim Radford + * 2004-11-08 0.8 fix init error case, don't return from a macro + * thanks to Chris Wright + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include "../dual_accel_detect.h" + +/* ThinkPad CMOS commands */ +#define TP_CMOS_VOLUME_DOWN 0 +#define TP_CMOS_VOLUME_UP 1 +#define TP_CMOS_VOLUME_MUTE 2 +#define TP_CMOS_BRIGHTNESS_UP 4 +#define TP_CMOS_BRIGHTNESS_DOWN 5 +#define TP_CMOS_THINKLIGHT_ON 12 +#define TP_CMOS_THINKLIGHT_OFF 13 + +/* NVRAM Addresses */ +enum tp_nvram_addr { + TP_NVRAM_ADDR_HK2 = 0x57, + TP_NVRAM_ADDR_THINKLIGHT = 0x58, + TP_NVRAM_ADDR_VIDEO = 0x59, + TP_NVRAM_ADDR_BRIGHTNESS = 0x5e, + TP_NVRAM_ADDR_MIXER = 0x60, +}; + +/* NVRAM bit masks */ +enum { + TP_NVRAM_MASK_HKT_THINKPAD = 0x08, + TP_NVRAM_MASK_HKT_ZOOM = 0x20, + TP_NVRAM_MASK_HKT_DISPLAY = 0x40, + TP_NVRAM_MASK_HKT_HIBERNATE = 0x80, + TP_NVRAM_MASK_THINKLIGHT = 0x10, + TP_NVRAM_MASK_HKT_DISPEXPND = 0x30, + TP_NVRAM_MASK_HKT_BRIGHTNESS = 0x20, + TP_NVRAM_MASK_LEVEL_BRIGHTNESS = 0x0f, + TP_NVRAM_POS_LEVEL_BRIGHTNESS = 0, + TP_NVRAM_MASK_MUTE = 0x40, + TP_NVRAM_MASK_HKT_VOLUME = 0x80, + TP_NVRAM_MASK_LEVEL_VOLUME = 0x0f, + TP_NVRAM_POS_LEVEL_VOLUME = 0, +}; + +/* Misc NVRAM-related */ +enum { + TP_NVRAM_LEVEL_VOLUME_MAX = 14, +}; + +/* ACPI HIDs */ +#define TPACPI_ACPI_IBM_HKEY_HID "IBM0068" +#define TPACPI_ACPI_LENOVO_HKEY_HID "LEN0068" +#define TPACPI_ACPI_LENOVO_HKEY_V2_HID "LEN0268" +#define TPACPI_ACPI_EC_HID "PNP0C09" + +/* Input IDs */ +#define TPACPI_HKEY_INPUT_PRODUCT 0x5054 /* "TP" */ +#define TPACPI_HKEY_INPUT_VERSION 0x4101 + +/* ACPI \WGSV commands */ +enum { + TP_ACPI_WGSV_GET_STATE = 0x01, /* Get state information */ + TP_ACPI_WGSV_PWR_ON_ON_RESUME = 0x02, /* Resume WWAN powered on */ + TP_ACPI_WGSV_PWR_OFF_ON_RESUME = 0x03, /* Resume WWAN powered off */ + TP_ACPI_WGSV_SAVE_STATE = 0x04, /* Save state for S4/S5 */ +}; + +/* TP_ACPI_WGSV_GET_STATE bits */ +enum { + TP_ACPI_WGSV_STATE_WWANEXIST = 0x0001, /* WWAN hw available */ + TP_ACPI_WGSV_STATE_WWANPWR = 0x0002, /* WWAN radio enabled */ + TP_ACPI_WGSV_STATE_WWANPWRRES = 0x0004, /* WWAN state at resume */ + TP_ACPI_WGSV_STATE_WWANBIOSOFF = 0x0008, /* WWAN disabled in BIOS */ + TP_ACPI_WGSV_STATE_BLTHEXIST = 0x0001, /* BLTH hw available */ + TP_ACPI_WGSV_STATE_BLTHPWR = 0x0002, /* BLTH radio enabled */ + TP_ACPI_WGSV_STATE_BLTHPWRRES = 0x0004, /* BLTH state at resume */ + TP_ACPI_WGSV_STATE_BLTHBIOSOFF = 0x0008, /* BLTH disabled in BIOS */ + TP_ACPI_WGSV_STATE_UWBEXIST = 0x0010, /* UWB hw available */ + TP_ACPI_WGSV_STATE_UWBPWR = 0x0020, /* UWB radio enabled */ +}; + +/* HKEY events */ +enum tpacpi_hkey_event_t { + /* Original hotkeys */ + TP_HKEY_EV_ORIG_KEY_START = 0x1001, /* First hotkey (FN+F1) */ + TP_HKEY_EV_BRGHT_UP = 0x1010, /* Brightness up */ + TP_HKEY_EV_BRGHT_DOWN = 0x1011, /* Brightness down */ + TP_HKEY_EV_KBD_LIGHT = 0x1012, /* Thinklight/kbd backlight */ + TP_HKEY_EV_VOL_UP = 0x1015, /* Volume up or unmute */ + TP_HKEY_EV_VOL_DOWN = 0x1016, /* Volume down or unmute */ + TP_HKEY_EV_VOL_MUTE = 0x1017, /* Mixer output mute */ + TP_HKEY_EV_ORIG_KEY_END = 0x1020, /* Last original hotkey code */ + + /* Adaptive keyboard (2014 X1 Carbon) */ + TP_HKEY_EV_DFR_CHANGE_ROW = 0x1101, /* Change adaptive kbd Fn row mode */ + TP_HKEY_EV_DFR_S_QUICKVIEW_ROW = 0x1102, /* Set adap. kbd Fn row to function mode */ + TP_HKEY_EV_ADAPTIVE_KEY_START = 0x1103, /* First hotkey code on adaptive kbd */ + TP_HKEY_EV_ADAPTIVE_KEY_END = 0x1116, /* Last hotkey code on adaptive kbd */ + + /* Extended hotkey events in 2017+ models */ + TP_HKEY_EV_EXTENDED_KEY_START = 0x1300, /* First extended hotkey code */ + TP_HKEY_EV_PRIVACYGUARD_TOGGLE = 0x130f, /* Toggle priv.guard on/off */ + TP_HKEY_EV_EXTENDED_KEY_END = 0x1319, /* Last extended hotkey code using + * hkey -> scancode translation for + * compat. Later codes are entered + * directly in the sparse-keymap. + */ + TP_HKEY_EV_AMT_TOGGLE = 0x131a, /* Toggle AMT on/off */ + TP_HKEY_EV_CAMERASHUTTER_TOGGLE = 0x131b, /* Toggle Camera Shutter */ + TP_HKEY_EV_DOUBLETAP_TOGGLE = 0x131c, /* Toggle trackpoint doubletap on/off */ + TP_HKEY_EV_PROFILE_TOGGLE = 0x131f, /* Toggle platform profile in 2024 systems */ + TP_HKEY_EV_PROFILE_TOGGLE2 = 0x1401, /* Toggle platform profile in 2025 + systems */ + + /* Reasons for waking up from S3/S4 */ + TP_HKEY_EV_WKUP_S3_UNDOCK = 0x2304, /* undock requested, S3 */ + TP_HKEY_EV_WKUP_S4_UNDOCK = 0x2404, /* undock requested, S4 */ + TP_HKEY_EV_WKUP_S3_BAYEJ = 0x2305, /* bay ejection req, S3 */ + TP_HKEY_EV_WKUP_S4_BAYEJ = 0x2405, /* bay ejection req, S4 */ + TP_HKEY_EV_WKUP_S3_BATLOW = 0x2313, /* battery empty, S3 */ + TP_HKEY_EV_WKUP_S4_BATLOW = 0x2413, /* battery empty, S4 */ + + /* Auto-sleep after eject request */ + TP_HKEY_EV_BAYEJ_ACK = 0x3003, /* bay ejection complete */ + TP_HKEY_EV_UNDOCK_ACK = 0x4003, /* undock complete */ + + /* Misc bay events */ + TP_HKEY_EV_OPTDRV_EJ = 0x3006, /* opt. drive tray ejected */ + TP_HKEY_EV_HOTPLUG_DOCK = 0x4010, /* docked into hotplug dock + or port replicator */ + TP_HKEY_EV_HOTPLUG_UNDOCK = 0x4011, /* undocked from hotplug + dock or port replicator */ + /* + * Thinkpad X1 Tablet series devices emit 0x4012 and 0x4013 + * when keyboard cover is attached, detached or folded onto the back + */ + TP_HKEY_EV_KBD_COVER_ATTACH = 0x4012, /* keyboard cover attached */ + TP_HKEY_EV_KBD_COVER_DETACH = 0x4013, /* keyboard cover detached or folded back */ + + /* User-interface events */ + TP_HKEY_EV_LID_CLOSE = 0x5001, /* laptop lid closed */ + TP_HKEY_EV_LID_OPEN = 0x5002, /* laptop lid opened */ + TP_HKEY_EV_TABLET_TABLET = 0x5009, /* tablet swivel up */ + TP_HKEY_EV_TABLET_NOTEBOOK = 0x500a, /* tablet swivel down */ + TP_HKEY_EV_TABLET_CHANGED = 0x60c0, /* X1 Yoga (2016): + * enter/leave tablet mode + */ + TP_HKEY_EV_PEN_INSERTED = 0x500b, /* tablet pen inserted */ + TP_HKEY_EV_PEN_REMOVED = 0x500c, /* tablet pen removed */ + TP_HKEY_EV_BRGHT_CHANGED = 0x5010, /* backlight control event */ + + /* Key-related user-interface events */ + TP_HKEY_EV_KEY_NUMLOCK = 0x6000, /* NumLock key pressed */ + TP_HKEY_EV_KEY_FN = 0x6005, /* Fn key pressed? E420 */ + TP_HKEY_EV_KEY_FN_ESC = 0x6060, /* Fn+Esc key pressed X240 */ + + /* Thermal events */ + TP_HKEY_EV_ALARM_BAT_HOT = 0x6011, /* battery too hot */ + TP_HKEY_EV_ALARM_BAT_XHOT = 0x6012, /* battery critically hot */ + TP_HKEY_EV_ALARM_BAT_LIM_CHANGE = 0x6013, /* battery charge limit changed*/ + TP_HKEY_EV_ALARM_SENSOR_HOT = 0x6021, /* sensor too hot */ + TP_HKEY_EV_ALARM_SENSOR_XHOT = 0x6022, /* sensor critically hot */ + TP_HKEY_EV_THM_TABLE_CHANGED = 0x6030, /* windows; thermal table changed */ + TP_HKEY_EV_THM_CSM_COMPLETED = 0x6032, /* windows; thermal control set + * command completed. Related to + * AML DYTC */ + TP_HKEY_EV_THM_TRANSFM_CHANGED = 0x60F0, /* windows; thermal transformation + * changed. Related to AML GMTS */ + + /* AC-related events */ + TP_HKEY_EV_AC_CHANGED = 0x6040, /* AC status changed */ + + /* Further user-interface events */ + TP_HKEY_EV_PALM_DETECTED = 0x60b0, /* palm hoveres keyboard */ + TP_HKEY_EV_PALM_UNDETECTED = 0x60b1, /* palm removed */ + + /* Misc */ + TP_HKEY_EV_RFKILL_CHANGED = 0x7000, /* rfkill switch changed */ + + /* Misc2 */ + TP_HKEY_EV_TRACK_DOUBLETAP = 0x8036, /* trackpoint doubletap */ +}; + +/**************************************************************************** + * Main driver + */ + +#define TPACPI_NAME "thinkpad" +#define TPACPI_DESC "ThinkPad ACPI Extras" +#define TPACPI_FILE TPACPI_NAME "_acpi" +#define TPACPI_URL "http://ibm-acpi.sf.net/" +#define TPACPI_MAIL "ibm-acpi-devel@lists.sourceforge.net" + +#define TPACPI_PROC_DIR "ibm" +#define TPACPI_ACPI_EVENT_PREFIX "ibm" +#define TPACPI_DRVR_NAME TPACPI_FILE +#define TPACPI_DRVR_SHORTNAME "tpacpi" +#define TPACPI_HWMON_DRVR_NAME TPACPI_NAME "_hwmon" + +#define TPACPI_NVRAM_KTHREAD_NAME "ktpacpi_nvramd" +#define TPACPI_WORKQUEUE_NAME "ktpacpid" + +#define TPACPI_MAX_ACPI_ARGS 3 + +/* Debugging printk groups */ +#define TPACPI_DBG_ALL 0xffff +#define TPACPI_DBG_DISCLOSETASK 0x8000 +#define TPACPI_DBG_INIT 0x0001 +#define TPACPI_DBG_EXIT 0x0002 +#define TPACPI_DBG_RFKILL 0x0004 +#define TPACPI_DBG_HKEY 0x0008 +#define TPACPI_DBG_FAN 0x0010 +#define TPACPI_DBG_BRGHT 0x0020 +#define TPACPI_DBG_MIXER 0x0040 + +#define FAN_NOT_PRESENT 65535 + +/**************************************************************************** + * Driver-wide structs and misc. variables + */ + +struct ibm_struct; + +struct tp_acpi_drv_struct { + const struct acpi_device_id *hid; + struct acpi_driver *driver; + + void (*notify) (struct ibm_struct *, u32); + acpi_handle *handle; + u32 type; + struct acpi_device *device; +}; + +struct ibm_struct { + char *name; + + int (*read) (struct seq_file *); + int (*write) (char *); + void (*exit) (void); + void (*resume) (void); + void (*suspend) (void); + void (*shutdown) (void); + + struct list_head all_drivers; + + struct tp_acpi_drv_struct *acpi; + + struct { + u8 acpi_driver_registered:1; + u8 acpi_notify_installed:1; + u8 proc_created:1; + u8 init_called:1; + u8 experimental:1; + } flags; +}; + +struct ibm_init_struct { + char param[32]; + + int (*init) (struct ibm_init_struct *); + umode_t base_procfs_mode; + struct ibm_struct *data; +}; + +/* DMI Quirks */ +struct quirk_entry { + bool btusb_bug; +}; + +static struct quirk_entry quirk_btusb_bug = { + .btusb_bug = true, +}; + +static struct { + u32 bluetooth:1; + u32 hotkey:1; + u32 hotkey_mask:1; + u32 hotkey_wlsw:1; + enum { + TP_HOTKEY_TABLET_NONE = 0, + TP_HOTKEY_TABLET_USES_MHKG, + TP_HOTKEY_TABLET_USES_GMMS, + } hotkey_tablet; + u32 kbdlight:1; + u32 light:1; + u32 light_status:1; + u32 bright_acpimode:1; + u32 bright_unkfw:1; + u32 wan:1; + u32 uwb:1; + u32 fan_ctrl_status_undef:1; + u32 second_fan:1; + u32 second_fan_ctl:1; + u32 beep_needs_two_args:1; + u32 mixer_no_level_control:1; + u32 battery_force_primary:1; + u32 platform_drv_registered:1; + u32 hotkey_poll_active:1; + u32 has_adaptive_kbd:1; + u32 kbd_lang:1; + u32 trackpoint_doubletap:1; + struct quirk_entry *quirks; +} tp_features; + +static struct { + u16 hotkey_mask_ff:1; + u16 volume_ctrl_forbidden:1; +} tp_warned; + +struct thinkpad_id_data { + unsigned int vendor; /* ThinkPad vendor: + * PCI_VENDOR_ID_IBM/PCI_VENDOR_ID_LENOVO */ + + char *bios_version_str; /* Something like 1ZET51WW (1.03z) */ + char *ec_version_str; /* Something like 1ZHT51WW-1.04a */ + + u32 bios_model; /* 1Y = 0x3159, 0 = unknown */ + u32 ec_model; + u16 bios_release; /* 1ZETK1WW = 0x4b31, 0 = unknown */ + u16 ec_release; + + char *model_str; /* ThinkPad T43 */ + char *nummodel_str; /* 9384A9C for a 9384-A9C model */ +}; +static struct thinkpad_id_data thinkpad_id; + +static enum { + TPACPI_LIFE_INIT = 0, + TPACPI_LIFE_RUNNING, + TPACPI_LIFE_EXITING, +} tpacpi_lifecycle; + +static int experimental; +static u32 dbg_level; + +static struct workqueue_struct *tpacpi_wq; + +enum led_status_t { + TPACPI_LED_OFF = 0, + TPACPI_LED_ON, + TPACPI_LED_BLINK, +}; + +/* tpacpi LED class */ +struct tpacpi_led_classdev { + struct led_classdev led_classdev; + int led; +}; + +/* brightness level capabilities */ +static unsigned int bright_maxlvl; /* 0 = unknown */ + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +static int dbg_wlswemul; +static bool tpacpi_wlsw_emulstate; +static int dbg_bluetoothemul; +static bool tpacpi_bluetooth_emulstate; +static int dbg_wwanemul; +static bool tpacpi_wwan_emulstate; +static int dbg_uwbemul; +static bool tpacpi_uwb_emulstate; +#endif + + +/************************************************************************* + * Debugging helpers + */ + +#define dbg_printk(a_dbg_level, format, arg...) \ +do { \ + if (dbg_level & (a_dbg_level)) \ + printk(KERN_DEBUG pr_fmt("%s: " format), \ + __func__, ##arg); \ +} while (0) + +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +#define vdbg_printk dbg_printk +static const char *str_supported(int is_supported); +#else +static inline const char *str_supported(int is_supported) { return ""; } +#define vdbg_printk(a_dbg_level, format, arg...) \ + do { if (0) no_printk(format, ##arg); } while (0) +#endif + +static void tpacpi_log_usertask(const char * const what) +{ + printk(KERN_DEBUG pr_fmt("%s: access by process with PID %d\n"), + what, task_tgid_vnr(current)); +} + +#define tpacpi_disclose_usertask(what, format, arg...) \ +do { \ + if (unlikely((dbg_level & TPACPI_DBG_DISCLOSETASK) && \ + (tpacpi_lifecycle == TPACPI_LIFE_RUNNING))) { \ + printk(KERN_DEBUG pr_fmt("%s: PID %d: " format), \ + what, task_tgid_vnr(current), ## arg); \ + } \ +} while (0) + +/* + * Quirk handling helpers + * + * ThinkPad IDs and versions seen in the field so far are + * two or three characters from the set [0-9A-Z], i.e. base 36. + * + * We use values well outside that range as specials. + */ + +#define TPACPI_MATCH_ANY 0xffffffffU +#define TPACPI_MATCH_ANY_VERSION 0xffffU +#define TPACPI_MATCH_UNKNOWN 0U + +/* TPID('1', 'Y') == 0x3159 */ +#define TPID(__c1, __c2) (((__c1) << 8) | (__c2)) +#define TPID3(__c1, __c2, __c3) (((__c1) << 16) | ((__c2) << 8) | (__c3)) +#define TPVER TPID + +#define TPACPI_Q_IBM(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_IBM, \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_Q_LNV(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_Q_LNV3(__id1, __id2, __id3, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPID3(__id1, __id2, __id3), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = (__quirk) } + +#define TPACPI_QEC_IBM(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_IBM, \ + .bios = TPACPI_MATCH_ANY, \ + .ec = TPID(__id1, __id2), \ + .quirks = (__quirk) } + +#define TPACPI_QEC_LNV(__id1, __id2, __quirk) \ + { .vendor = PCI_VENDOR_ID_LENOVO, \ + .bios = TPACPI_MATCH_ANY, \ + .ec = TPID(__id1, __id2), \ + .quirks = (__quirk) } + +struct tpacpi_quirk { + unsigned int vendor; + u32 bios; + u32 ec; + unsigned long quirks; +}; + +/** + * tpacpi_check_quirks() - search BIOS/EC version on a list + * @qlist: array of &struct tpacpi_quirk + * @qlist_size: number of elements in @qlist + * + * Iterates over a quirks list until one is found that matches the + * ThinkPad's vendor, BIOS and EC model. + * + * Returns: %0 if nothing matches, otherwise returns the quirks field of + * the matching &struct tpacpi_quirk entry. + * + * The match criteria is: vendor, ec and bios must match. + */ +static unsigned long __init tpacpi_check_quirks( + const struct tpacpi_quirk *qlist, + unsigned int qlist_size) +{ + while (qlist_size) { + if ((qlist->vendor == thinkpad_id.vendor || + qlist->vendor == TPACPI_MATCH_ANY) && + (qlist->bios == thinkpad_id.bios_model || + qlist->bios == TPACPI_MATCH_ANY) && + (qlist->ec == thinkpad_id.ec_model || + qlist->ec == TPACPI_MATCH_ANY)) + return qlist->quirks; + + qlist_size--; + qlist++; + } + return 0; +} + +static inline bool __pure __init tpacpi_is_lenovo(void) +{ + return thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO; +} + +static inline bool __pure __init tpacpi_is_ibm(void) +{ + return thinkpad_id.vendor == PCI_VENDOR_ID_IBM; +} + +/**************************************************************************** + **************************************************************************** + * + * ACPI Helpers and device model + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * ACPI basic handles + */ + +static acpi_handle root_handle; +static acpi_handle ec_handle; + +#define TPACPI_HANDLE(object, parent, paths...) \ + static acpi_handle object##_handle; \ + static const acpi_handle * const object##_parent __initconst = \ + &parent##_handle; \ + static char *object##_paths[] __initdata = { paths } + +TPACPI_HANDLE(ecrd, ec, "ECRD"); /* 570 */ +TPACPI_HANDLE(ecwr, ec, "ECWR"); /* 570 */ + +TPACPI_HANDLE(cmos, root, "\\UCMS", /* R50, R50e, R50p, R51, */ + /* T4x, X31, X40 */ + "\\CMOS", /* A3x, G4x, R32, T23, T30, X22-24, X30 */ + "\\CMS", /* R40, R40e */ + ); /* all others */ + +TPACPI_HANDLE(hkey, ec, "\\_SB.HKEY", /* 600e/x, 770e, 770x */ + "^HKEY", /* R30, R31 */ + "HKEY", /* all others */ + ); /* 570 */ + +/************************************************************************* + * ACPI helpers + */ + +static int acpi_evalf(acpi_handle handle, + int *res, char *method, char *fmt, ...) +{ + char *fmt0 = fmt; + struct acpi_object_list params; + union acpi_object in_objs[TPACPI_MAX_ACPI_ARGS]; + struct acpi_buffer result, *resultp; + union acpi_object out_obj; + acpi_status status; + va_list ap; + char res_type; + int success; + int quiet; + + if (!*fmt) { + pr_err("acpi_evalf() called with empty format\n"); + return 0; + } + + if (*fmt == 'q') { + quiet = 1; + fmt++; + } else + quiet = 0; + + res_type = *(fmt++); + + params.count = 0; + params.pointer = &in_objs[0]; + + va_start(ap, fmt); + while (*fmt) { + char c = *(fmt++); + switch (c) { + case 'd': /* int */ + in_objs[params.count].integer.value = va_arg(ap, int); + in_objs[params.count++].type = ACPI_TYPE_INTEGER; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", + c); + va_end(ap); + return 0; + } + } + va_end(ap); + + if (res_type != 'v') { + result.length = sizeof(out_obj); + result.pointer = &out_obj; + resultp = &result; + } else + resultp = NULL; + + status = acpi_evaluate_object(handle, method, ¶ms, resultp); + + switch (res_type) { + case 'd': /* int */ + success = (status == AE_OK && + out_obj.type == ACPI_TYPE_INTEGER); + if (success && res) + *res = out_obj.integer.value; + break; + case 'v': /* void */ + success = status == AE_OK; + break; + /* add more types as needed */ + default: + pr_err("acpi_evalf() called with invalid format character '%c'\n", + res_type); + return 0; + } + + if (!success && !quiet) + pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", + method, fmt0, acpi_format_exception(status)); + + return success; +} + +static int acpi_ec_read(int i, u8 *p) +{ + int v; + + if (ecrd_handle) { + if (!acpi_evalf(ecrd_handle, &v, NULL, "dd", i)) + return 0; + *p = v; + } else { + if (ec_read(i, p) < 0) + return 0; + } + + return 1; +} + +static int acpi_ec_write(int i, u8 v) +{ + if (ecwr_handle) { + if (!acpi_evalf(ecwr_handle, NULL, NULL, "vdd", i, v)) + return 0; + } else { + if (ec_write(i, v) < 0) + return 0; + } + + return 1; +} + +static int issue_thinkpad_cmos_command(int cmos_cmd) +{ + if (!cmos_handle) + return -ENXIO; + + if (!acpi_evalf(cmos_handle, NULL, NULL, "vd", cmos_cmd)) + return -EIO; + + return 0; +} + +/************************************************************************* + * ACPI device model + */ + +#define TPACPI_ACPIHANDLE_INIT(object) \ + drv_acpi_handle_init(#object, &object##_handle, *object##_parent, \ + object##_paths, ARRAY_SIZE(object##_paths)) + +static void __init drv_acpi_handle_init(const char *name, + acpi_handle *handle, const acpi_handle parent, + char **paths, const int num_paths) +{ + int i; + acpi_status status; + + vdbg_printk(TPACPI_DBG_INIT, "trying to locate ACPI handle for %s\n", + name); + + for (i = 0; i < num_paths; i++) { + status = acpi_get_handle(parent, paths[i], handle); + if (ACPI_SUCCESS(status)) { + dbg_printk(TPACPI_DBG_INIT, + "Found ACPI handle %s for %s\n", + paths[i], name); + return; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "ACPI handle for %s not found\n", + name); + *handle = NULL; +} + +static acpi_status __init tpacpi_acpi_handle_locate_callback(acpi_handle handle, + u32 level, void *context, void **return_value) +{ + if (!strcmp(context, "video")) { + struct acpi_device *dev = acpi_fetch_acpi_dev(handle); + + if (!dev || strcmp(ACPI_VIDEO_HID, acpi_device_hid(dev))) + return AE_OK; + } + + *(acpi_handle *)return_value = handle; + + return AE_CTRL_TERMINATE; +} + +static void __init tpacpi_acpi_handle_locate(const char *name, + const char *hid, + acpi_handle *handle) +{ + acpi_status status; + acpi_handle device_found; + + BUG_ON(!name || !handle); + vdbg_printk(TPACPI_DBG_INIT, + "trying to locate ACPI handle for %s, using HID %s\n", + name, hid ? hid : "NULL"); + + memset(&device_found, 0, sizeof(device_found)); + status = acpi_get_devices(hid, tpacpi_acpi_handle_locate_callback, + (void *)name, &device_found); + + *handle = NULL; + + if (ACPI_SUCCESS(status)) { + *handle = device_found; + dbg_printk(TPACPI_DBG_INIT, + "Found ACPI handle for %s\n", name); + } else { + vdbg_printk(TPACPI_DBG_INIT, + "Could not locate an ACPI handle for %s: %s\n", + name, acpi_format_exception(status)); + } +} + +static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) +{ + struct ibm_struct *ibm = data; + + if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) + return; + + if (!ibm || !ibm->acpi || !ibm->acpi->notify) + return; + + ibm->acpi->notify(ibm, event); +} + +static int __init setup_acpi_notify(struct ibm_struct *ibm) +{ + acpi_status status; + + BUG_ON(!ibm->acpi); + + if (!*ibm->acpi->handle) + return 0; + + vdbg_printk(TPACPI_DBG_INIT, + "setting up ACPI notify for %s\n", ibm->name); + + ibm->acpi->device = acpi_fetch_acpi_dev(*ibm->acpi->handle); + if (!ibm->acpi->device) { + pr_err("acpi_fetch_acpi_dev(%s) failed\n", ibm->name); + return -ENODEV; + } + + ibm->acpi->device->driver_data = ibm; + scnprintf(acpi_device_class(ibm->acpi->device), + sizeof(acpi_device_class(ibm->acpi->device)), + "%s/%s", TPACPI_ACPI_EVENT_PREFIX, ibm->name); + + status = acpi_install_notify_handler(*ibm->acpi->handle, + ibm->acpi->type, dispatch_acpi_notify, ibm); + if (ACPI_FAILURE(status)) { + if (status == AE_ALREADY_EXISTS) { + pr_notice("another device driver is already handling %s events\n", + ibm->name); + } else { + pr_err("acpi_install_notify_handler(%s) failed: %s\n", + ibm->name, acpi_format_exception(status)); + } + return -ENODEV; + } + ibm->flags.acpi_notify_installed = 1; + return 0; +} + +static int __init tpacpi_device_add(struct acpi_device *device) +{ + return 0; +} + +static int __init register_tpacpi_subdriver(struct ibm_struct *ibm) +{ + int rc; + + dbg_printk(TPACPI_DBG_INIT, + "registering %s as an ACPI driver\n", ibm->name); + + BUG_ON(!ibm->acpi); + + ibm->acpi->driver = kzalloc(sizeof(struct acpi_driver), GFP_KERNEL); + if (!ibm->acpi->driver) { + pr_err("failed to allocate memory for ibm->acpi->driver\n"); + return -ENOMEM; + } + + sprintf(ibm->acpi->driver->name, "%s_%s", TPACPI_NAME, ibm->name); + ibm->acpi->driver->ids = ibm->acpi->hid; + + ibm->acpi->driver->ops.add = &tpacpi_device_add; + + rc = acpi_bus_register_driver(ibm->acpi->driver); + if (rc < 0) { + pr_err("acpi_bus_register_driver(%s) failed: %d\n", + ibm->name, rc); + kfree(ibm->acpi->driver); + ibm->acpi->driver = NULL; + } else if (!rc) + ibm->flags.acpi_driver_registered = 1; + + return rc; +} + + +/**************************************************************************** + **************************************************************************** + * + * Procfs Helpers + * + **************************************************************************** + ****************************************************************************/ + +static int dispatch_proc_show(struct seq_file *m, void *v) +{ + struct ibm_struct *ibm = m->private; + + if (!ibm || !ibm->read) + return -EINVAL; + return ibm->read(m); +} + +static int dispatch_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, dispatch_proc_show, pde_data(inode)); +} + +static ssize_t dispatch_proc_write(struct file *file, + const char __user *userbuf, + size_t count, loff_t *pos) +{ + struct ibm_struct *ibm = pde_data(file_inode(file)); + char *kernbuf; + int ret; + + if (!ibm || !ibm->write) + return -EINVAL; + if (count > PAGE_SIZE - 1) + return -EINVAL; + + kernbuf = memdup_user_nul(userbuf, count); + if (IS_ERR(kernbuf)) + return PTR_ERR(kernbuf); + ret = ibm->write(kernbuf); + if (ret == 0) + ret = count; + + kfree(kernbuf); + + return ret; +} + +static const struct proc_ops dispatch_proc_ops = { + .proc_open = dispatch_proc_open, + .proc_read = seq_read, + .proc_lseek = seq_lseek, + .proc_release = single_release, + .proc_write = dispatch_proc_write, +}; + +/**************************************************************************** + **************************************************************************** + * + * Device model: input, hwmon and platform + * + **************************************************************************** + ****************************************************************************/ + +static struct platform_device *tpacpi_pdev; +static struct platform_device *tpacpi_sensors_pdev; +static struct device *tpacpi_hwmon; +static struct device *tpacpi_pprof; +static struct input_dev *tpacpi_inputdev; +static struct mutex tpacpi_inputdev_send_mutex; +static LIST_HEAD(tpacpi_all_drivers); + +#ifdef CONFIG_PM_SLEEP +static int tpacpi_suspend_handler(struct device *dev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->suspend) + (ibm->suspend)(); + } + + return 0; +} + +static int tpacpi_resume_handler(struct device *dev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->resume) + (ibm->resume)(); + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(tpacpi_pm, + tpacpi_suspend_handler, tpacpi_resume_handler); + +static void tpacpi_shutdown_handler(struct platform_device *pdev) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe(ibm, itmp, + &tpacpi_all_drivers, + all_drivers) { + if (ibm->shutdown) + (ibm->shutdown)(); + } +} + +/************************************************************************* + * sysfs support helpers + */ + +static int parse_strtoul(const char *buf, + unsigned long max, unsigned long *value) +{ + char *endp; + + *value = simple_strtoul(skip_spaces(buf), &endp, 0); + endp = skip_spaces(endp); + if (*endp || *value > max) + return -EINVAL; + + return 0; +} + +static void tpacpi_disable_brightness_delay(void) +{ + if (acpi_evalf(hkey_handle, NULL, "PWMS", "qvd", 0)) + pr_notice("ACPI backlight control delay disabled\n"); +} + +static void printk_deprecated_attribute(const char * const what, + const char * const details) +{ + tpacpi_log_usertask("deprecated sysfs attribute"); + pr_warn("WARNING: sysfs attribute %s is deprecated and will be removed. %s\n", + what, details); +} + +/************************************************************************* + * rfkill and radio control support helpers + */ + +/* + * ThinkPad-ACPI firmware handling model: + * + * WLSW (master wireless switch) is event-driven, and is common to all + * firmware-controlled radios. It cannot be controlled, just monitored, + * as expected. It overrides all radio state in firmware + * + * The kernel, a masked-off hotkey, and WLSW can change the radio state + * (TODO: verify how WLSW interacts with the returned radio state). + * + * The only time there are shadow radio state changes, is when + * masked-off hotkeys are used. + */ + +/* + * Internal driver API for radio state: + * + * int: < 0 = error, otherwise enum tpacpi_rfkill_state + * bool: true means radio blocked (off) + */ +enum tpacpi_rfkill_state { + TPACPI_RFK_RADIO_OFF = 0, + TPACPI_RFK_RADIO_ON +}; + +/* rfkill switches */ +enum tpacpi_rfk_id { + TPACPI_RFK_BLUETOOTH_SW_ID = 0, + TPACPI_RFK_WWAN_SW_ID, + TPACPI_RFK_UWB_SW_ID, + TPACPI_RFK_SW_MAX +}; + +static const char *tpacpi_rfkill_names[] = { + [TPACPI_RFK_BLUETOOTH_SW_ID] = "bluetooth", + [TPACPI_RFK_WWAN_SW_ID] = "wwan", + [TPACPI_RFK_UWB_SW_ID] = "uwb", + [TPACPI_RFK_SW_MAX] = NULL +}; + +/* ThinkPad-ACPI rfkill subdriver */ +struct tpacpi_rfk { + struct rfkill *rfkill; + enum tpacpi_rfk_id id; + const struct tpacpi_rfk_ops *ops; +}; + +struct tpacpi_rfk_ops { + /* firmware interface */ + int (*get_status)(void); + int (*set_status)(const enum tpacpi_rfkill_state); +}; + +static struct tpacpi_rfk *tpacpi_rfkill_switches[TPACPI_RFK_SW_MAX]; + +/* Query FW and update rfkill sw state for a given rfkill switch */ +static int tpacpi_rfk_update_swstate(const struct tpacpi_rfk *tp_rfk) +{ + int status; + + if (!tp_rfk) + return -ENODEV; + + status = (tp_rfk->ops->get_status)(); + if (status < 0) + return status; + + rfkill_set_sw_state(tp_rfk->rfkill, + (status == TPACPI_RFK_RADIO_OFF)); + + return status; +} + +/* + * Sync the HW-blocking state of all rfkill switches, + * do notice it causes the rfkill core to schedule uevents + */ +static void tpacpi_rfk_update_hwblock_state(bool blocked) +{ + unsigned int i; + struct tpacpi_rfk *tp_rfk; + + for (i = 0; i < TPACPI_RFK_SW_MAX; i++) { + tp_rfk = tpacpi_rfkill_switches[i]; + if (tp_rfk) { + if (rfkill_set_hw_state(tp_rfk->rfkill, + blocked)) { + /* ignore -- we track sw block */ + } + } + } +} + +/* Call to get the WLSW state from the firmware */ +static int hotkey_get_wlsw(void); + +/* Call to query WLSW state and update all rfkill switches */ +static bool tpacpi_rfk_check_hwblock_state(void) +{ + int res = hotkey_get_wlsw(); + int hw_blocked; + + /* When unknown or unsupported, we have to assume it is unblocked */ + if (res < 0) + return false; + + hw_blocked = (res == TPACPI_RFK_RADIO_OFF); + tpacpi_rfk_update_hwblock_state(hw_blocked); + + return hw_blocked; +} + +static int tpacpi_rfk_hook_set_block(void *data, bool blocked) +{ + struct tpacpi_rfk *tp_rfk = data; + int res; + + dbg_printk(TPACPI_DBG_RFKILL, + "request to change radio state to %s\n", + blocked ? "blocked" : "unblocked"); + + /* try to set radio state */ + res = (tp_rfk->ops->set_status)(blocked ? + TPACPI_RFK_RADIO_OFF : TPACPI_RFK_RADIO_ON); + + /* and update the rfkill core with whatever the FW really did */ + tpacpi_rfk_update_swstate(tp_rfk); + + return (res < 0) ? res : 0; +} + +static const struct rfkill_ops tpacpi_rfk_rfkill_ops = { + .set_block = tpacpi_rfk_hook_set_block, +}; + +static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id, + const struct tpacpi_rfk_ops *tp_rfkops, + const enum rfkill_type rfktype, + const char *name, + const bool set_default) +{ + struct tpacpi_rfk *atp_rfk; + int res; + bool sw_state = false; + bool hw_state; + int sw_status; + + BUG_ON(id >= TPACPI_RFK_SW_MAX || tpacpi_rfkill_switches[id]); + + atp_rfk = kzalloc(sizeof(struct tpacpi_rfk), GFP_KERNEL); + if (atp_rfk) + atp_rfk->rfkill = rfkill_alloc(name, + &tpacpi_pdev->dev, + rfktype, + &tpacpi_rfk_rfkill_ops, + atp_rfk); + if (!atp_rfk || !atp_rfk->rfkill) { + pr_err("failed to allocate memory for rfkill class\n"); + kfree(atp_rfk); + return -ENOMEM; + } + + atp_rfk->id = id; + atp_rfk->ops = tp_rfkops; + + sw_status = (tp_rfkops->get_status)(); + if (sw_status < 0) { + pr_err("failed to read initial state for %s, error %d\n", + name, sw_status); + } else { + sw_state = (sw_status == TPACPI_RFK_RADIO_OFF); + if (set_default) { + /* try to keep the initial state, since we ask the + * firmware to preserve it across S5 in NVRAM */ + rfkill_init_sw_state(atp_rfk->rfkill, sw_state); + } + } + hw_state = tpacpi_rfk_check_hwblock_state(); + rfkill_set_hw_state(atp_rfk->rfkill, hw_state); + + res = rfkill_register(atp_rfk->rfkill); + if (res < 0) { + pr_err("failed to register %s rfkill switch: %d\n", name, res); + rfkill_destroy(atp_rfk->rfkill); + kfree(atp_rfk); + return res; + } + + tpacpi_rfkill_switches[id] = atp_rfk; + + pr_info("rfkill switch %s: radio is %sblocked\n", + name, (sw_state || hw_state) ? "" : "un"); + return 0; +} + +static void tpacpi_destroy_rfkill(const enum tpacpi_rfk_id id) +{ + struct tpacpi_rfk *tp_rfk; + + BUG_ON(id >= TPACPI_RFK_SW_MAX); + + tp_rfk = tpacpi_rfkill_switches[id]; + if (tp_rfk) { + rfkill_unregister(tp_rfk->rfkill); + rfkill_destroy(tp_rfk->rfkill); + tpacpi_rfkill_switches[id] = NULL; + kfree(tp_rfk); + } +} + +static void printk_deprecated_rfkill_attribute(const char * const what) +{ + printk_deprecated_attribute(what, + "Please switch to generic rfkill before year 2010"); +} + +/* sysfs enable ------------------------------------------------ */ +static ssize_t tpacpi_rfk_sysfs_enable_show(const enum tpacpi_rfk_id id, + struct device_attribute *attr, + char *buf) +{ + int status; + + printk_deprecated_rfkill_attribute(attr->attr.name); + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state()) { + status = TPACPI_RFK_RADIO_OFF; + } else { + status = tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + if (status < 0) + return status; + } + + return sysfs_emit(buf, "%d\n", + (status == TPACPI_RFK_RADIO_ON) ? 1 : 0); +} + +static ssize_t tpacpi_rfk_sysfs_enable_store(const enum tpacpi_rfk_id id, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + printk_deprecated_rfkill_attribute(attr->attr.name); + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_disclose_usertask(attr->attr.name, "set to %ld\n", t); + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state() && !!t) + return -EPERM; + + res = tpacpi_rfkill_switches[id]->ops->set_status((!!t) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF); + tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + + return (res < 0) ? res : count; +} + +/* procfs -------------------------------------------------------------- */ +static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, struct seq_file *m) +{ + if (id >= TPACPI_RFK_SW_MAX) + seq_printf(m, "status:\t\tnot supported\n"); + else { + int status; + + /* This is in the ABI... */ + if (tpacpi_rfk_check_hwblock_state()) { + status = TPACPI_RFK_RADIO_OFF; + } else { + status = tpacpi_rfk_update_swstate( + tpacpi_rfkill_switches[id]); + if (status < 0) + return status; + } + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status == TPACPI_RFK_RADIO_ON)); + seq_printf(m, "commands:\tenable, disable\n"); + } + + return 0; +} + +static int tpacpi_rfk_procfs_write(const enum tpacpi_rfk_id id, char *buf) +{ + char *cmd; + int status = -1; + int res = 0; + + if (id >= TPACPI_RFK_SW_MAX) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "enable")) + status = TPACPI_RFK_RADIO_ON; + else if (strstarts(cmd, "disable")) + status = TPACPI_RFK_RADIO_OFF; + else + return -EINVAL; + } + + if (status != -1) { + tpacpi_disclose_usertask("procfs", "attempt to %s %s\n", + str_enable_disable(status == TPACPI_RFK_RADIO_ON), + tpacpi_rfkill_names[id]); + res = (tpacpi_rfkill_switches[id]->ops->set_status)(status); + tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); + } + + return res; +} + +/************************************************************************* + * thinkpad-acpi driver attributes + */ + +/* interface_version --------------------------------------------------- */ +static ssize_t interface_version_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", TPACPI_SYSFS_VERSION); +} +static DRIVER_ATTR_RO(interface_version); + +/* debug_level --------------------------------------------------------- */ +static ssize_t debug_level_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "0x%04x\n", dbg_level); +} + +static ssize_t debug_level_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 0xffff, &t)) + return -EINVAL; + + dbg_level = t; + + return count; +} +static DRIVER_ATTR_RW(debug_level); + +/* version ------------------------------------------------------------- */ +static ssize_t version_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%s v%s\n", + TPACPI_DESC, TPACPI_VERSION); +} +static DRIVER_ATTR_RO(version); + +/* --------------------------------------------------------------------- */ + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + +/* wlsw_emulstate ------------------------------------------------------ */ +static ssize_t wlsw_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_wlsw_emulstate); +} + +static ssize_t wlsw_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + if (tpacpi_wlsw_emulstate != !!t) { + tpacpi_wlsw_emulstate = !!t; + tpacpi_rfk_update_hwblock_state(!t); /* negative logic */ + } + + return count; +} +static DRIVER_ATTR_RW(wlsw_emulstate); + +/* bluetooth_emulstate ------------------------------------------------- */ +static ssize_t bluetooth_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_bluetooth_emulstate); +} + +static ssize_t bluetooth_emulstate_store(struct device_driver *drv, + const char *buf, size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_bluetooth_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(bluetooth_emulstate); + +/* wwan_emulstate ------------------------------------------------- */ +static ssize_t wwan_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_wwan_emulstate); +} + +static ssize_t wwan_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_wwan_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(wwan_emulstate); + +/* uwb_emulstate ------------------------------------------------- */ +static ssize_t uwb_emulstate_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%d\n", !!tpacpi_uwb_emulstate); +} + +static ssize_t uwb_emulstate_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + tpacpi_uwb_emulstate = !!t; + + return count; +} +static DRIVER_ATTR_RW(uwb_emulstate); +#endif + +/************************************************************************* + * Firmware Data + */ + +/* + * Table of recommended minimum BIOS versions + * + * Reasons for listing: + * 1. Stable BIOS, listed because the unknown amount of + * bugs and bad ACPI behaviour on older versions + * + * 2. BIOS or EC fw with known bugs that trigger on Linux + * + * 3. BIOS with known reduced functionality in older versions + * + * We recommend the latest BIOS and EC version. + * We only support the latest BIOS and EC fw version as a rule. + * + * Sources: IBM ThinkPad Public Web Documents (update changelogs), + * Information from users in ThinkWiki + * + * WARNING: we use this table also to detect that the machine is + * a ThinkPad in some cases, so don't remove entries lightly. + */ + +#define TPV_Q(__v, __id1, __id2, __bv1, __bv2) \ + { .vendor = (__v), \ + .bios = TPID(__id1, __id2), \ + .ec = TPACPI_MATCH_ANY, \ + .quirks = TPACPI_MATCH_ANY_VERSION << 16 \ + | TPVER(__bv1, __bv2) } + +#define TPV_Q_X(__v, __bid1, __bid2, __bv1, __bv2, \ + __eid, __ev1, __ev2) \ + { .vendor = (__v), \ + .bios = TPID(__bid1, __bid2), \ + .ec = __eid, \ + .quirks = TPVER(__ev1, __ev2) << 16 \ + | TPVER(__bv1, __bv2) } + +#define TPV_QI0(__id1, __id2, __bv1, __bv2) \ + TPV_Q(PCI_VENDOR_ID_IBM, __id1, __id2, __bv1, __bv2) + +/* Outdated IBM BIOSes often lack the EC id string */ +#define TPV_QI1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ + __bv1, __bv2, TPID(__id1, __id2), \ + __ev1, __ev2), \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ + __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ + __ev1, __ev2) + +/* Outdated IBM BIOSes often lack the EC id string */ +#define TPV_QI2(__bid1, __bid2, __bv1, __bv2, \ + __eid1, __eid2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ + __bv1, __bv2, TPID(__eid1, __eid2), \ + __ev1, __ev2), \ + TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ + __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ + __ev1, __ev2) + +#define TPV_QL0(__id1, __id2, __bv1, __bv2) \ + TPV_Q(PCI_VENDOR_ID_LENOVO, __id1, __id2, __bv1, __bv2) + +#define TPV_QL1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_LENOVO, __id1, __id2, \ + __bv1, __bv2, TPID(__id1, __id2), \ + __ev1, __ev2) + +#define TPV_QL2(__bid1, __bid2, __bv1, __bv2, \ + __eid1, __eid2, __ev1, __ev2) \ + TPV_Q_X(PCI_VENDOR_ID_LENOVO, __bid1, __bid2, \ + __bv1, __bv2, TPID(__eid1, __eid2), \ + __ev1, __ev2) + +static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = { + /* Numeric models ------------------ */ + /* FW MODEL BIOS VERS */ + TPV_QI0('I', 'M', '6', '5'), /* 570 */ + TPV_QI0('I', 'U', '2', '6'), /* 570E */ + TPV_QI0('I', 'B', '5', '4'), /* 600 */ + TPV_QI0('I', 'H', '4', '7'), /* 600E */ + TPV_QI0('I', 'N', '3', '6'), /* 600E */ + TPV_QI0('I', 'T', '5', '5'), /* 600X */ + TPV_QI0('I', 'D', '4', '8'), /* 770, 770E, 770ED */ + TPV_QI0('I', 'I', '4', '2'), /* 770X */ + TPV_QI0('I', 'O', '2', '3'), /* 770Z */ + + /* A-series ------------------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('I', 'W', '5', '9'), /* A20m */ + TPV_QI0('I', 'V', '6', '9'), /* A20p */ + TPV_QI0('1', '0', '2', '6'), /* A21e, A22e */ + TPV_QI0('K', 'U', '3', '6'), /* A21e */ + TPV_QI0('K', 'X', '3', '6'), /* A21m, A22m */ + TPV_QI0('K', 'Y', '3', '8'), /* A21p, A22p */ + TPV_QI0('1', 'B', '1', '7'), /* A22e */ + TPV_QI0('1', '3', '2', '0'), /* A22m */ + TPV_QI0('1', 'E', '7', '3'), /* A30/p (0) */ + TPV_QI1('1', 'G', '4', '1', '1', '7'), /* A31/p (0) */ + TPV_QI1('1', 'N', '1', '6', '0', '7'), /* A31/p (0) */ + + /* G-series ------------------------- */ + /* FW MODEL BIOS VERS */ + TPV_QI0('1', 'T', 'A', '6'), /* G40 */ + TPV_QI0('1', 'X', '5', '7'), /* G41 */ + + /* R-series, T-series --------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('1', 'C', 'F', '0'), /* R30 */ + TPV_QI0('1', 'F', 'F', '1'), /* R31 */ + TPV_QI0('1', 'M', '9', '7'), /* R32 */ + TPV_QI0('1', 'O', '6', '1'), /* R40 */ + TPV_QI0('1', 'P', '6', '5'), /* R40 */ + TPV_QI0('1', 'S', '7', '0'), /* R40e */ + TPV_QI1('1', 'R', 'D', 'R', '7', '1'), /* R50/p, R51, + T40/p, T41/p, T42/p (1) */ + TPV_QI1('1', 'V', '7', '1', '2', '8'), /* R50e, R51 (1) */ + TPV_QI1('7', '8', '7', '1', '0', '6'), /* R51e (1) */ + TPV_QI1('7', '6', '6', '9', '1', '6'), /* R52 (1) */ + TPV_QI1('7', '0', '6', '9', '2', '8'), /* R52, T43 (1) */ + + TPV_QI0('I', 'Y', '6', '1'), /* T20 */ + TPV_QI0('K', 'Z', '3', '4'), /* T21 */ + TPV_QI0('1', '6', '3', '2'), /* T22 */ + TPV_QI1('1', 'A', '6', '4', '2', '3'), /* T23 (0) */ + TPV_QI1('1', 'I', '7', '1', '2', '0'), /* T30 (0) */ + TPV_QI1('1', 'Y', '6', '5', '2', '9'), /* T43/p (1) */ + + TPV_QL1('7', '9', 'E', '3', '5', '0'), /* T60/p */ + TPV_QL1('7', 'C', 'D', '2', '2', '2'), /* R60, R60i */ + TPV_QL1('7', 'E', 'D', '0', '1', '5'), /* R60e, R60i */ + + /* BIOS FW BIOS VERS EC FW EC VERS */ + TPV_QI2('1', 'W', '9', '0', '1', 'V', '2', '8'), /* R50e (1) */ + TPV_QL2('7', 'I', '3', '4', '7', '9', '5', '0'), /* T60/p wide */ + + /* X-series ------------------------- */ + /* FW MODEL BIOS VERS EC VERS */ + TPV_QI0('I', 'Z', '9', 'D'), /* X20, X21 */ + TPV_QI0('1', 'D', '7', '0'), /* X22, X23, X24 */ + TPV_QI1('1', 'K', '4', '8', '1', '8'), /* X30 (0) */ + TPV_QI1('1', 'Q', '9', '7', '2', '3'), /* X31, X32 (0) */ + TPV_QI1('1', 'U', 'D', '3', 'B', '2'), /* X40 (0) */ + TPV_QI1('7', '4', '6', '4', '2', '7'), /* X41 (0) */ + TPV_QI1('7', '5', '6', '0', '2', '0'), /* X41t (0) */ + + TPV_QL1('7', 'B', 'D', '7', '4', '0'), /* X60/s */ + TPV_QL1('7', 'J', '3', '0', '1', '3'), /* X60t */ + + /* (0) - older versions lack DMI EC fw string and functionality */ + /* (1) - older versions known to lack functionality */ +}; + +#undef TPV_QL1 +#undef TPV_QL0 +#undef TPV_QI2 +#undef TPV_QI1 +#undef TPV_QI0 +#undef TPV_Q_X +#undef TPV_Q + +static void __init tpacpi_check_outdated_fw(void) +{ + unsigned long fwvers; + u16 ec_version, bios_version; + + fwvers = tpacpi_check_quirks(tpacpi_bios_version_qtable, + ARRAY_SIZE(tpacpi_bios_version_qtable)); + + if (!fwvers) + return; + + bios_version = fwvers & 0xffffU; + ec_version = (fwvers >> 16) & 0xffffU; + + /* note that unknown versions are set to 0x0000 and we use that */ + if ((bios_version > thinkpad_id.bios_release) || + (ec_version > thinkpad_id.ec_release && + ec_version != TPACPI_MATCH_ANY_VERSION)) { + /* + * The changelogs would let us track down the exact + * reason, but it is just too much of a pain to track + * it. We only list BIOSes that are either really + * broken, or really stable to begin with, so it is + * best if the user upgrades the firmware anyway. + */ + pr_warn("WARNING: Outdated ThinkPad BIOS/EC firmware\n"); + pr_warn("WARNING: This firmware may be missing critical bug fixes and/or important features\n"); + } +} + +static bool __init tpacpi_is_fw_known(void) +{ + return tpacpi_check_quirks(tpacpi_bios_version_qtable, + ARRAY_SIZE(tpacpi_bios_version_qtable)) != 0; +} + +/**************************************************************************** + **************************************************************************** + * + * Subdrivers + * + **************************************************************************** + ****************************************************************************/ + +/************************************************************************* + * thinkpad-acpi metadata subdriver + */ + +static int thinkpad_acpi_driver_read(struct seq_file *m) +{ + seq_printf(m, "driver:\t\t%s\n", TPACPI_DESC); + seq_printf(m, "version:\t%s\n", TPACPI_VERSION); + return 0; +} + +static struct ibm_struct thinkpad_acpi_driver_data = { + .name = "driver", + .read = thinkpad_acpi_driver_read, +}; + +/************************************************************************* + * Hotkey subdriver + */ + +/* + * ThinkPad firmware event model + * + * The ThinkPad firmware has two main event interfaces: normal ACPI + * notifications (which follow the ACPI standard), and a private event + * interface. + * + * The private event interface also issues events for the hotkeys. As + * the driver gained features, the event handling code ended up being + * built around the hotkey subdriver. This will need to be refactored + * to a more formal event API eventually. + * + * Some "hotkeys" are actually supposed to be used as event reports, + * such as "brightness has changed", "volume has changed", depending on + * the ThinkPad model and how the firmware is operating. + * + * Unlike other classes, hotkey-class events have mask/unmask control on + * non-ancient firmware. However, how it behaves changes a lot with the + * firmware model and version. + */ + +enum { /* hot key scan codes (derived from ACPI DSDT) */ + TP_ACPI_HOTKEYSCAN_FNF1 = 0, + TP_ACPI_HOTKEYSCAN_FNF2, + TP_ACPI_HOTKEYSCAN_FNF3, + TP_ACPI_HOTKEYSCAN_FNF4, + TP_ACPI_HOTKEYSCAN_FNF5, + TP_ACPI_HOTKEYSCAN_FNF6, + TP_ACPI_HOTKEYSCAN_FNF7, + TP_ACPI_HOTKEYSCAN_FNF8, + TP_ACPI_HOTKEYSCAN_FNF9, + TP_ACPI_HOTKEYSCAN_FNF10, + TP_ACPI_HOTKEYSCAN_FNF11, + TP_ACPI_HOTKEYSCAN_FNF12, + TP_ACPI_HOTKEYSCAN_FNBACKSPACE, + TP_ACPI_HOTKEYSCAN_FNINSERT, + TP_ACPI_HOTKEYSCAN_FNDELETE, + TP_ACPI_HOTKEYSCAN_FNHOME, + TP_ACPI_HOTKEYSCAN_FNEND, + TP_ACPI_HOTKEYSCAN_FNPAGEUP, + TP_ACPI_HOTKEYSCAN_FNPAGEDOWN, + TP_ACPI_HOTKEYSCAN_FNSPACE, + TP_ACPI_HOTKEYSCAN_VOLUMEUP, + TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, + TP_ACPI_HOTKEYSCAN_MUTE, + TP_ACPI_HOTKEYSCAN_THINKPAD, + TP_ACPI_HOTKEYSCAN_UNK1, + TP_ACPI_HOTKEYSCAN_UNK2, + TP_ACPI_HOTKEYSCAN_MICMUTE, + TP_ACPI_HOTKEYSCAN_UNK4, + TP_ACPI_HOTKEYSCAN_CONFIG, + TP_ACPI_HOTKEYSCAN_SEARCH, + TP_ACPI_HOTKEYSCAN_SCALE, + TP_ACPI_HOTKEYSCAN_FILE, + + /* Adaptive keyboard keycodes */ + TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, /* 32 / 0x20 */ + TP_ACPI_HOTKEYSCAN_MUTE2 = TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, + TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, + TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, + TP_ACPI_HOTKEYSCAN_CLOUD, + TP_ACPI_HOTKEYSCAN_UNK9, + TP_ACPI_HOTKEYSCAN_VOICE, + TP_ACPI_HOTKEYSCAN_UNK10, + TP_ACPI_HOTKEYSCAN_GESTURES, + TP_ACPI_HOTKEYSCAN_UNK11, + TP_ACPI_HOTKEYSCAN_UNK12, + TP_ACPI_HOTKEYSCAN_UNK13, + TP_ACPI_HOTKEYSCAN_CONFIG2, + TP_ACPI_HOTKEYSCAN_NEW_TAB, + TP_ACPI_HOTKEYSCAN_RELOAD, + TP_ACPI_HOTKEYSCAN_BACK, + TP_ACPI_HOTKEYSCAN_MIC_DOWN, + TP_ACPI_HOTKEYSCAN_MIC_UP, + TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, + TP_ACPI_HOTKEYSCAN_CAMERA_MODE, + TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, + + /* Lenovo extended keymap, starting at 0x1300 */ + TP_ACPI_HOTKEYSCAN_EXTENDED_START, /* 52 / 0x34 */ + /* first new observed key (star, favorites) is 0x1311 */ + TP_ACPI_HOTKEYSCAN_STAR = 69, + TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, + TP_ACPI_HOTKEYSCAN_CALCULATOR, + TP_ACPI_HOTKEYSCAN_BLUETOOTH, + TP_ACPI_HOTKEYSCAN_KEYBOARD, + TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, /* Used by "Lenovo Quick Clean" */ + TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, + TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, + TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, +}; + +enum { /* Keys/events available through NVRAM polling */ + TPACPI_HKEY_NVRAM_KNOWN_MASK = 0x00fb88c0U, + TPACPI_HKEY_NVRAM_GOOD_MASK = 0x00fb8000U, +}; + +enum { /* Positions of some of the keys in hotkey masks */ + TP_ACPI_HKEY_DISPSWTCH_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF7, + TP_ACPI_HKEY_DISPXPAND_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF8, + TP_ACPI_HKEY_HIBERNATE_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF12, + TP_ACPI_HKEY_BRGHTUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNHOME, + TP_ACPI_HKEY_BRGHTDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNEND, + TP_ACPI_HKEY_KBD_LIGHT_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNPAGEUP, + TP_ACPI_HKEY_ZOOM_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNSPACE, + TP_ACPI_HKEY_VOLUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEUP, + TP_ACPI_HKEY_VOLDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, + TP_ACPI_HKEY_MUTE_MASK = 1 << TP_ACPI_HOTKEYSCAN_MUTE, + TP_ACPI_HKEY_THINKPAD_MASK = 1 << TP_ACPI_HOTKEYSCAN_THINKPAD, +}; + +enum { /* NVRAM to ACPI HKEY group map */ + TP_NVRAM_HKEY_GROUP_HK2 = TP_ACPI_HKEY_THINKPAD_MASK | + TP_ACPI_HKEY_ZOOM_MASK | + TP_ACPI_HKEY_DISPSWTCH_MASK | + TP_ACPI_HKEY_HIBERNATE_MASK, + TP_NVRAM_HKEY_GROUP_BRIGHTNESS = TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK, + TP_NVRAM_HKEY_GROUP_VOLUME = TP_ACPI_HKEY_VOLUP_MASK | + TP_ACPI_HKEY_VOLDWN_MASK | + TP_ACPI_HKEY_MUTE_MASK, +}; + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +struct tp_nvram_state { + u16 thinkpad_toggle:1; + u16 zoom_toggle:1; + u16 display_toggle:1; + u16 thinklight_toggle:1; + u16 hibernate_toggle:1; + u16 displayexp_toggle:1; + u16 display_state:1; + u16 brightness_toggle:1; + u16 volume_toggle:1; + u16 mute:1; + + u8 brightness_level; + u8 volume_level; +}; + +/* kthread for the hotkey poller */ +static struct task_struct *tpacpi_hotkey_task; + +/* + * Acquire mutex to write poller control variables as an + * atomic block. + * + * Increment hotkey_config_change when changing them if you + * want the kthread to forget old state. + * + * See HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END + */ +static struct mutex hotkey_thread_data_mutex; +static unsigned int hotkey_config_change; + +/* + * hotkey poller control variables + * + * Must be atomic or readers will also need to acquire mutex + * + * HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END + * should be used only when the changes need to be taken as + * a block, OR when one needs to force the kthread to forget + * old state. + */ +static u32 hotkey_source_mask; /* bit mask 0=ACPI,1=NVRAM */ +static unsigned int hotkey_poll_freq = 10; /* Hz */ + +#define HOTKEY_CONFIG_CRITICAL_START \ + do { \ + mutex_lock(&hotkey_thread_data_mutex); \ + hotkey_config_change++; \ + } while (0); +#define HOTKEY_CONFIG_CRITICAL_END \ + mutex_unlock(&hotkey_thread_data_mutex); + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +#define hotkey_source_mask 0U +#define HOTKEY_CONFIG_CRITICAL_START +#define HOTKEY_CONFIG_CRITICAL_END + +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static struct mutex hotkey_mutex; + +static enum { /* Reasons for waking up */ + TP_ACPI_WAKEUP_NONE = 0, /* None or unknown */ + TP_ACPI_WAKEUP_BAYEJ, /* Bay ejection request */ + TP_ACPI_WAKEUP_UNDOCK, /* Undock request */ +} hotkey_wakeup_reason; + +static int hotkey_autosleep_ack; + +static u32 hotkey_orig_mask; /* events the BIOS had enabled */ +static u32 hotkey_all_mask; /* all events supported in fw */ +static u32 hotkey_adaptive_all_mask; /* all adaptive events supported in fw */ +static u32 hotkey_reserved_mask; /* events better left disabled */ +static u32 hotkey_driver_mask; /* events needed by the driver */ +static u32 hotkey_user_mask; /* events visible to userspace */ +static u32 hotkey_acpi_mask; /* events enabled in firmware */ + +static bool tpacpi_driver_event(const unsigned int hkey_event); +static void hotkey_poll_setup(const bool may_warn); + +/* HKEY.MHKG() return bits */ +#define TP_HOTKEY_TABLET_MASK (1 << 3) +enum { + TP_ACPI_MULTI_MODE_INVALID = 0, + TP_ACPI_MULTI_MODE_UNKNOWN = 1 << 0, + TP_ACPI_MULTI_MODE_LAPTOP = 1 << 1, + TP_ACPI_MULTI_MODE_TABLET = 1 << 2, + TP_ACPI_MULTI_MODE_FLAT = 1 << 3, + TP_ACPI_MULTI_MODE_STAND = 1 << 4, + TP_ACPI_MULTI_MODE_TENT = 1 << 5, + TP_ACPI_MULTI_MODE_STAND_TENT = 1 << 6, +}; + +enum { + /* The following modes are considered tablet mode for the purpose of + * reporting the status to userspace. i.e. in all these modes it makes + * sense to disable the laptop input devices such as touchpad and + * keyboard. + */ + TP_ACPI_MULTI_MODE_TABLET_LIKE = TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT | + TP_ACPI_MULTI_MODE_STAND_TENT, +}; + +static int hotkey_get_wlsw(void) +{ + int status; + + if (!tp_features.hotkey_wlsw) + return -ENODEV; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wlswemul) + return (tpacpi_wlsw_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "WLSW", "d")) + return -EIO; + + return (status) ? TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int hotkey_gmms_get_tablet_mode(int s, int *has_tablet_mode) +{ + int type = (s >> 16) & 0xffff; + int value = s & 0xffff; + int mode = TP_ACPI_MULTI_MODE_INVALID; + int valid_modes = 0; + + if (has_tablet_mode) + *has_tablet_mode = 0; + + switch (type) { + case 1: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND_TENT; + break; + case 2: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT; + break; + case 3: + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT; + break; + case 4: + case 5: + /* In mode 4, FLAT is not specified as a valid mode. However, + * it can be seen at least on the X1 Yoga 2nd Generation. + */ + valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | + TP_ACPI_MULTI_MODE_FLAT | + TP_ACPI_MULTI_MODE_TABLET | + TP_ACPI_MULTI_MODE_STAND | + TP_ACPI_MULTI_MODE_TENT; + break; + default: + pr_err("Unknown multi mode status type %d with value 0x%04X, please report this to %s\n", + type, value, TPACPI_MAIL); + return 0; + } + + if (has_tablet_mode && (valid_modes & TP_ACPI_MULTI_MODE_TABLET_LIKE)) + *has_tablet_mode = 1; + + switch (value) { + case 1: + mode = TP_ACPI_MULTI_MODE_LAPTOP; + break; + case 2: + mode = TP_ACPI_MULTI_MODE_FLAT; + break; + case 3: + mode = TP_ACPI_MULTI_MODE_TABLET; + break; + case 4: + if (type == 1) + mode = TP_ACPI_MULTI_MODE_STAND_TENT; + else + mode = TP_ACPI_MULTI_MODE_STAND; + break; + case 5: + mode = TP_ACPI_MULTI_MODE_TENT; + break; + default: + if (type == 5 && value == 0xffff) { + pr_warn("Multi mode status is undetected, assuming laptop\n"); + return 0; + } + } + + if (!(mode & valid_modes)) { + pr_err("Unknown/reserved multi mode value 0x%04X for type %d, please report this to %s\n", + value, type, TPACPI_MAIL); + return 0; + } + + return !!(mode & TP_ACPI_MULTI_MODE_TABLET_LIKE); +} + +static int hotkey_get_tablet_mode(int *status) +{ + int s; + + switch (tp_features.hotkey_tablet) { + case TP_HOTKEY_TABLET_USES_MHKG: + if (!acpi_evalf(hkey_handle, &s, "MHKG", "d")) + return -EIO; + + *status = ((s & TP_HOTKEY_TABLET_MASK) != 0); + break; + case TP_HOTKEY_TABLET_USES_GMMS: + if (!acpi_evalf(hkey_handle, &s, "GMMS", "dd", 0)) + return -EIO; + + *status = hotkey_gmms_get_tablet_mode(s, NULL); + break; + default: + break; + } + + return 0; +} + +/* + * Reads current event mask from firmware, and updates + * hotkey_acpi_mask accordingly. Also resets any bits + * from hotkey_user_mask that are unavailable to be + * delivered (shadow requirement of the userspace ABI). + */ +static int hotkey_mask_get(void) +{ + lockdep_assert_held(&hotkey_mutex); + + if (tp_features.hotkey_mask) { + u32 m = 0; + + if (!acpi_evalf(hkey_handle, &m, "DHKN", "d")) + return -EIO; + + hotkey_acpi_mask = m; + } else { + /* no mask support doesn't mean no event support... */ + hotkey_acpi_mask = hotkey_all_mask; + } + + /* sync userspace-visible mask */ + hotkey_user_mask &= (hotkey_acpi_mask | hotkey_source_mask); + + return 0; +} + +static void hotkey_mask_warn_incomplete_mask(void) +{ + /* log only what the user can fix... */ + const u32 wantedmask = hotkey_driver_mask & + ~(hotkey_acpi_mask | hotkey_source_mask) & + (hotkey_all_mask | TPACPI_HKEY_NVRAM_KNOWN_MASK); + + if (wantedmask) + pr_notice("required events 0x%08x not enabled!\n", wantedmask); +} + +/* + * Set the firmware mask when supported + * + * Also calls hotkey_mask_get to update hotkey_acpi_mask. + * + * NOTE: does not set bits in hotkey_user_mask, but may reset them. + */ +static int hotkey_mask_set(u32 mask) +{ + int i; + int rc = 0; + + const u32 fwmask = mask & ~hotkey_source_mask; + + lockdep_assert_held(&hotkey_mutex); + + if (tp_features.hotkey_mask) { + for (i = 0; i < 32; i++) { + if (!acpi_evalf(hkey_handle, + NULL, "MHKM", "vdd", i + 1, + !!(mask & (1 << i)))) { + rc = -EIO; + break; + } + } + } + + /* + * We *must* make an inconditional call to hotkey_mask_get to + * refresh hotkey_acpi_mask and update hotkey_user_mask + * + * Take the opportunity to also log when we cannot _enable_ + * a given event. + */ + if (!hotkey_mask_get() && !rc && (fwmask & ~hotkey_acpi_mask)) { + pr_notice("asked for hotkey mask 0x%08x, but firmware forced it to 0x%08x\n", + fwmask, hotkey_acpi_mask); + } + + if (tpacpi_lifecycle != TPACPI_LIFE_EXITING) + hotkey_mask_warn_incomplete_mask(); + + return rc; +} + +/* + * Sets hotkey_user_mask and tries to set the firmware mask + */ +static int hotkey_user_mask_set(const u32 mask) +{ + int rc; + + lockdep_assert_held(&hotkey_mutex); + + /* Give people a chance to notice they are doing something that + * is bound to go boom on their users sooner or later */ + if (!tp_warned.hotkey_mask_ff && + (mask == 0xffff || mask == 0xffffff || + mask == 0xffffffff)) { + tp_warned.hotkey_mask_ff = 1; + pr_notice("setting the hotkey mask to 0x%08x is likely not the best way to go about it\n", + mask); + pr_notice("please consider using the driver defaults, and refer to up-to-date thinkpad-acpi documentation\n"); + } + + /* Try to enable what the user asked for, plus whatever we need. + * this syncs everything but won't enable bits in hotkey_user_mask */ + rc = hotkey_mask_set((mask | hotkey_driver_mask) & ~hotkey_source_mask); + + /* Enable the available bits in hotkey_user_mask */ + hotkey_user_mask = mask & (hotkey_acpi_mask | hotkey_source_mask); + + return rc; +} + +/* + * Sets the driver hotkey mask. + * + * Can be called even if the hotkey subdriver is inactive + */ +static int tpacpi_hotkey_driver_mask_set(const u32 mask) +{ + int rc; + + /* Do the right thing if hotkey_init has not been called yet */ + if (!tp_features.hotkey) { + hotkey_driver_mask = mask; + return 0; + } + + mutex_lock(&hotkey_mutex); + + HOTKEY_CONFIG_CRITICAL_START + hotkey_driver_mask = mask; +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_source_mask |= (mask & ~hotkey_all_mask); +#endif + HOTKEY_CONFIG_CRITICAL_END + + rc = hotkey_mask_set((hotkey_acpi_mask | hotkey_driver_mask) & + ~hotkey_source_mask); + hotkey_poll_setup(true); + + mutex_unlock(&hotkey_mutex); + + return rc; +} + +static int hotkey_status_get(int *status) +{ + if (!acpi_evalf(hkey_handle, status, "DHKC", "d")) + return -EIO; + + return 0; +} + +static int hotkey_status_set(bool enable) +{ + if (!acpi_evalf(hkey_handle, NULL, "MHKC", "vd", enable ? 1 : 0)) + return -EIO; + + return 0; +} + +static void tpacpi_input_send_tabletsw(void) +{ + int state; + + if (tp_features.hotkey_tablet && + !hotkey_get_tablet_mode(&state)) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, + SW_TABLET_MODE, !!state); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } +} + +#define GCES_NO_SHUTTER_DEVICE BIT(31) + +static int get_camera_shutter(void) +{ + acpi_handle gces_handle; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GCES", &gces_handle))) + return -ENODEV; + + if (!acpi_evalf(gces_handle, &output, NULL, "dd", 0)) + return -EIO; + + if (output & GCES_NO_SHUTTER_DEVICE) + return -ENODEV; + + return output; +} + +static bool tpacpi_input_send_key(const u32 hkey, bool *send_acpi_ev) +{ + bool known_ev; + u32 scancode; + + if (tpacpi_driver_event(hkey)) + return true; + + /* + * Before the conversion to using the sparse-keymap helpers the driver used to + * map the hkey event codes to 0x00 - 0x4d scancodes so that a straight scancode + * indexed array could be used to map scancodes to keycodes: + * + * 0x1001 - 0x1020 -> 0x00 - 0x1f (Original ThinkPad events) + * 0x1103 - 0x1116 -> 0x20 - 0x33 (Adaptive keyboard, 2014 X1 Carbon) + * 0x1300 - 0x1319 -> 0x34 - 0x4d (Additional keys send in 2017+ models) + * + * The sparse-keymap tables still use these scancodes for these ranges to + * preserve userspace API compatibility (e.g. hwdb keymappings). + */ + if (hkey >= TP_HKEY_EV_ORIG_KEY_START && + hkey <= TP_HKEY_EV_ORIG_KEY_END) { + scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; + if (!(hotkey_user_mask & (1 << scancode))) + return true; /* Not reported but still a known code */ + } else if (hkey >= TP_HKEY_EV_ADAPTIVE_KEY_START && + hkey <= TP_HKEY_EV_ADAPTIVE_KEY_END) { + scancode = hkey - TP_HKEY_EV_ADAPTIVE_KEY_START + + TP_ACPI_HOTKEYSCAN_ADAPTIVE_START; + } else if (hkey >= TP_HKEY_EV_EXTENDED_KEY_START && + hkey <= TP_HKEY_EV_EXTENDED_KEY_END) { + scancode = hkey - TP_HKEY_EV_EXTENDED_KEY_START + + TP_ACPI_HOTKEYSCAN_EXTENDED_START; + } else { + /* + * Do not send ACPI netlink events for unknown hotkeys, to + * avoid userspace starting to rely on them. Instead these + * should be added to the keymap to send evdev events. + */ + if (send_acpi_ev) + *send_acpi_ev = false; + + scancode = hkey; + } + + mutex_lock(&tpacpi_inputdev_send_mutex); + known_ev = sparse_keymap_report_event(tpacpi_inputdev, scancode, 1, true); + mutex_unlock(&tpacpi_inputdev_send_mutex); + + return known_ev; +} + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver; + +/* Do NOT call without validating scancode first */ +static void tpacpi_hotkey_send_key(unsigned int scancode) +{ + tpacpi_input_send_key(TP_HKEY_EV_ORIG_KEY_START + scancode, NULL); +} + +static void hotkey_read_nvram(struct tp_nvram_state *n, const u32 m) +{ + u8 d; + + if (m & TP_NVRAM_HKEY_GROUP_HK2) { + d = nvram_read_byte(TP_NVRAM_ADDR_HK2); + n->thinkpad_toggle = !!(d & TP_NVRAM_MASK_HKT_THINKPAD); + n->zoom_toggle = !!(d & TP_NVRAM_MASK_HKT_ZOOM); + n->display_toggle = !!(d & TP_NVRAM_MASK_HKT_DISPLAY); + n->hibernate_toggle = !!(d & TP_NVRAM_MASK_HKT_HIBERNATE); + } + if (m & TP_ACPI_HKEY_KBD_LIGHT_MASK) { + d = nvram_read_byte(TP_NVRAM_ADDR_THINKLIGHT); + n->thinklight_toggle = !!(d & TP_NVRAM_MASK_THINKLIGHT); + } + if (m & TP_ACPI_HKEY_DISPXPAND_MASK) { + d = nvram_read_byte(TP_NVRAM_ADDR_VIDEO); + n->displayexp_toggle = + !!(d & TP_NVRAM_MASK_HKT_DISPEXPND); + } + if (m & TP_NVRAM_HKEY_GROUP_BRIGHTNESS) { + d = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); + n->brightness_level = (d & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; + n->brightness_toggle = + !!(d & TP_NVRAM_MASK_HKT_BRIGHTNESS); + } + if (m & TP_NVRAM_HKEY_GROUP_VOLUME) { + d = nvram_read_byte(TP_NVRAM_ADDR_MIXER); + n->volume_level = (d & TP_NVRAM_MASK_LEVEL_VOLUME) + >> TP_NVRAM_POS_LEVEL_VOLUME; + n->mute = !!(d & TP_NVRAM_MASK_MUTE); + n->volume_toggle = !!(d & TP_NVRAM_MASK_HKT_VOLUME); + } +} + +#define TPACPI_COMPARE_KEY(__scancode, __member) \ +do { \ + if ((event_mask & (1 << __scancode)) && \ + oldn->__member != newn->__member) \ + tpacpi_hotkey_send_key(__scancode); \ +} while (0) + +#define TPACPI_MAY_SEND_KEY(__scancode) \ +do { \ + if (event_mask & (1 << __scancode)) \ + tpacpi_hotkey_send_key(__scancode); \ +} while (0) + +static void issue_volchange(const unsigned int oldvol, + const unsigned int newvol, + const u32 event_mask) +{ + unsigned int i = oldvol; + + while (i > newvol) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + i--; + } + while (i < newvol) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + i++; + } +} + +static void issue_brightnesschange(const unsigned int oldbrt, + const unsigned int newbrt, + const u32 event_mask) +{ + unsigned int i = oldbrt; + + while (i > newbrt) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + i--; + } + while (i < newbrt) { + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + i++; + } +} + +static void hotkey_compare_and_issue_event(struct tp_nvram_state *oldn, + struct tp_nvram_state *newn, + const u32 event_mask) +{ + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_THINKPAD, thinkpad_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNSPACE, zoom_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF7, display_toggle); + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF12, hibernate_toggle); + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNPAGEUP, thinklight_toggle); + + TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF8, displayexp_toggle); + + /* + * Handle volume + * + * This code is supposed to duplicate the IBM firmware behaviour: + * - Pressing MUTE issues mute hotkey message, even when already mute + * - Pressing Volume up/down issues volume up/down hotkey messages, + * even when already at maximum or minimum volume + * - The act of unmuting issues volume up/down notification, + * depending which key was used to unmute + * + * We are constrained to what the NVRAM can tell us, which is not much + * and certainly not enough if more than one volume hotkey was pressed + * since the last poll cycle. + * + * Just to make our life interesting, some newer Lenovo ThinkPads have + * bugs in the BIOS and may fail to update volume_toggle properly. + */ + if (newn->mute) { + /* muted */ + if (!oldn->mute || + oldn->volume_toggle != newn->volume_toggle || + oldn->volume_level != newn->volume_level) { + /* recently muted, or repeated mute keypress, or + * multiple presses ending in mute */ + issue_volchange(oldn->volume_level, newn->volume_level, + event_mask); + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); + } + } else { + /* unmute */ + if (oldn->mute) { + /* recently unmuted, issue 'unmute' keypress */ + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } + if (oldn->volume_level != newn->volume_level) { + issue_volchange(oldn->volume_level, newn->volume_level, + event_mask); + } else if (oldn->volume_toggle != newn->volume_toggle) { + /* repeated vol up/down keypress at end of scale ? */ + if (newn->volume_level == 0) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); + else if (newn->volume_level >= TP_NVRAM_LEVEL_VOLUME_MAX) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); + } + } + + /* handle brightness */ + if (oldn->brightness_level != newn->brightness_level) { + issue_brightnesschange(oldn->brightness_level, + newn->brightness_level, event_mask); + } else if (oldn->brightness_toggle != newn->brightness_toggle) { + /* repeated key presses that didn't change state */ + if (newn->brightness_level == 0) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); + else if (newn->brightness_level >= bright_maxlvl + && !tp_features.bright_unkfw) + TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); + } + +#undef TPACPI_COMPARE_KEY +#undef TPACPI_MAY_SEND_KEY +} + +/* + * Polling driver + * + * We track all events in hotkey_source_mask all the time, since + * most of them are edge-based. We only issue those requested by + * hotkey_user_mask or hotkey_driver_mask, though. + */ +static int hotkey_kthread(void *data) +{ + struct tp_nvram_state s[2] = { 0 }; + u32 poll_mask, event_mask; + unsigned int si, so; + unsigned long t; + unsigned int change_detector; + unsigned int poll_freq; + bool was_frozen; + + if (tpacpi_lifecycle == TPACPI_LIFE_EXITING) + goto exit; + + set_freezable(); + + so = 0; + si = 1; + t = 0; + + /* Initial state for compares */ + mutex_lock(&hotkey_thread_data_mutex); + change_detector = hotkey_config_change; + poll_mask = hotkey_source_mask; + event_mask = hotkey_source_mask & + (hotkey_driver_mask | hotkey_user_mask); + poll_freq = hotkey_poll_freq; + mutex_unlock(&hotkey_thread_data_mutex); + hotkey_read_nvram(&s[so], poll_mask); + + while (!kthread_should_stop()) { + if (t == 0) { + if (likely(poll_freq)) + t = 1000/poll_freq; + else + t = 100; /* should never happen... */ + } + t = msleep_interruptible(t); + if (unlikely(kthread_freezable_should_stop(&was_frozen))) + break; + + if (t > 0 && !was_frozen) + continue; + + mutex_lock(&hotkey_thread_data_mutex); + if (was_frozen || hotkey_config_change != change_detector) { + /* forget old state on thaw or config change */ + si = so; + t = 0; + change_detector = hotkey_config_change; + } + poll_mask = hotkey_source_mask; + event_mask = hotkey_source_mask & + (hotkey_driver_mask | hotkey_user_mask); + poll_freq = hotkey_poll_freq; + mutex_unlock(&hotkey_thread_data_mutex); + + if (likely(poll_mask)) { + hotkey_read_nvram(&s[si], poll_mask); + if (likely(si != so)) { + hotkey_compare_and_issue_event(&s[so], &s[si], + event_mask); + } + } + + so = si; + si ^= 1; + } + +exit: + return 0; +} + +static void hotkey_poll_stop_sync(void) +{ + lockdep_assert_held(&hotkey_mutex); + + if (tpacpi_hotkey_task) { + kthread_stop(tpacpi_hotkey_task); + tpacpi_hotkey_task = NULL; + } +} + +static void hotkey_poll_setup(const bool may_warn) +{ + const u32 poll_driver_mask = hotkey_driver_mask & hotkey_source_mask; + const u32 poll_user_mask = hotkey_user_mask & hotkey_source_mask; + + lockdep_assert_held(&hotkey_mutex); + + if (hotkey_poll_freq > 0 && + (poll_driver_mask || + (poll_user_mask && tpacpi_inputdev->users > 0))) { + if (!tpacpi_hotkey_task) { + tpacpi_hotkey_task = kthread_run(hotkey_kthread, + NULL, TPACPI_NVRAM_KTHREAD_NAME); + if (IS_ERR(tpacpi_hotkey_task)) { + tpacpi_hotkey_task = NULL; + pr_err("could not create kernel thread for hotkey polling\n"); + } + } + } else { + hotkey_poll_stop_sync(); + if (may_warn && (poll_driver_mask || poll_user_mask) && + hotkey_poll_freq == 0) { + pr_notice("hot keys 0x%08x and/or events 0x%08x require polling, which is currently disabled\n", + poll_user_mask, poll_driver_mask); + } + } +} + +static void hotkey_poll_setup_safe(const bool may_warn) +{ + mutex_lock(&hotkey_mutex); + hotkey_poll_setup(may_warn); + mutex_unlock(&hotkey_mutex); +} + +static void hotkey_poll_set_freq(unsigned int freq) +{ + lockdep_assert_held(&hotkey_mutex); + + if (!freq) + hotkey_poll_stop_sync(); + + hotkey_poll_freq = freq; +} + +#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static void hotkey_poll_setup(const bool __unused) +{ +} + +static void hotkey_poll_setup_safe(const bool __unused) +{ +} + +static void hotkey_poll_stop_sync(void) +{ +} +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +static int hotkey_inputdev_open(struct input_dev *dev) +{ + switch (tpacpi_lifecycle) { + case TPACPI_LIFE_INIT: + case TPACPI_LIFE_RUNNING: + hotkey_poll_setup_safe(false); + return 0; + case TPACPI_LIFE_EXITING: + return -EBUSY; + } + + /* Should only happen if tpacpi_lifecycle is corrupt */ + BUG(); + return -EBUSY; +} + +static void hotkey_inputdev_close(struct input_dev *dev) +{ + /* disable hotkey polling when possible */ + if (tpacpi_lifecycle != TPACPI_LIFE_EXITING && + !(hotkey_source_mask & hotkey_driver_mask)) + hotkey_poll_setup_safe(false); +} + +/* sysfs hotkey enable ------------------------------------------------- */ +static ssize_t hotkey_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, status; + + printk_deprecated_attribute("hotkey_enable", + "Hotkey reporting is always enabled"); + + res = hotkey_status_get(&status); + if (res) + return res; + + return sysfs_emit(buf, "%d\n", status); +} + +static ssize_t hotkey_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + + printk_deprecated_attribute("hotkey_enable", + "Hotkeys can be disabled through hotkey_mask"); + + if (parse_strtoul(buf, 1, &t)) + return -EINVAL; + + if (t == 0) + return -EPERM; + + return count; +} + +static DEVICE_ATTR_RW(hotkey_enable); + +/* sysfs hotkey mask --------------------------------------------------- */ +static ssize_t hotkey_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", hotkey_user_mask); +} + +static ssize_t hotkey_mask_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, 0xffffffffUL, &t)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + res = hotkey_user_mask_set(t); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_poll_setup(true); +#endif + + mutex_unlock(&hotkey_mutex); + + tpacpi_disclose_usertask("hotkey_mask", "set to 0x%08lx\n", t); + + return (res) ? res : count; +} + +static DEVICE_ATTR_RW(hotkey_mask); + +/* sysfs hotkey bios_enabled ------------------------------------------- */ +static ssize_t hotkey_bios_enabled_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static DEVICE_ATTR_RO(hotkey_bios_enabled); + +/* sysfs hotkey bios_mask ---------------------------------------------- */ +static ssize_t hotkey_bios_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + printk_deprecated_attribute("hotkey_bios_mask", + "This attribute is useless."); + return sysfs_emit(buf, "0x%08x\n", hotkey_orig_mask); +} + +static DEVICE_ATTR_RO(hotkey_bios_mask); + +/* sysfs hotkey all_mask ----------------------------------------------- */ +static ssize_t hotkey_all_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + hotkey_all_mask | hotkey_source_mask); +} + +static DEVICE_ATTR_RO(hotkey_all_mask); + +/* sysfs hotkey all_mask ----------------------------------------------- */ +static ssize_t hotkey_adaptive_all_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + hotkey_adaptive_all_mask | hotkey_source_mask); +} + +static DEVICE_ATTR_RO(hotkey_adaptive_all_mask); + +/* sysfs hotkey recommended_mask --------------------------------------- */ +static ssize_t hotkey_recommended_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", + (hotkey_all_mask | hotkey_source_mask) + & ~hotkey_reserved_mask); +} + +static DEVICE_ATTR_RO(hotkey_recommended_mask); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + +/* sysfs hotkey hotkey_source_mask ------------------------------------- */ +static ssize_t hotkey_source_mask_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0x%08x\n", hotkey_source_mask); +} + +static ssize_t hotkey_source_mask_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + u32 r_ev; + int rc; + + if (parse_strtoul(buf, 0xffffffffUL, &t) || + ((t & ~TPACPI_HKEY_NVRAM_KNOWN_MASK) != 0)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + HOTKEY_CONFIG_CRITICAL_START + hotkey_source_mask = t; + HOTKEY_CONFIG_CRITICAL_END + + rc = hotkey_mask_set((hotkey_user_mask | hotkey_driver_mask) & + ~hotkey_source_mask); + hotkey_poll_setup(true); + + /* check if events needed by the driver got disabled */ + r_ev = hotkey_driver_mask & ~(hotkey_acpi_mask & hotkey_all_mask) + & ~hotkey_source_mask & TPACPI_HKEY_NVRAM_KNOWN_MASK; + + mutex_unlock(&hotkey_mutex); + + if (rc < 0) + pr_err("hotkey_source_mask: failed to update the firmware event mask!\n"); + + if (r_ev) + pr_notice("hotkey_source_mask: some important events were disabled: 0x%04x\n", + r_ev); + + tpacpi_disclose_usertask("hotkey_source_mask", "set to 0x%08lx\n", t); + + return (rc < 0) ? rc : count; +} + +static DEVICE_ATTR_RW(hotkey_source_mask); + +/* sysfs hotkey hotkey_poll_freq --------------------------------------- */ +static ssize_t hotkey_poll_freq_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_poll_freq); +} + +static ssize_t hotkey_poll_freq_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 25, &t)) + return -EINVAL; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + hotkey_poll_set_freq(t); + hotkey_poll_setup(true); + + mutex_unlock(&hotkey_mutex); + + tpacpi_disclose_usertask("hotkey_poll_freq", "set to %lu\n", t); + + return count; +} + +static DEVICE_ATTR_RW(hotkey_poll_freq); + +#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ + +/* sysfs hotkey radio_sw (pollable) ------------------------------------ */ +static ssize_t hotkey_radio_sw_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + res = hotkey_get_wlsw(); + if (res < 0) + return res; + + /* Opportunistic update */ + tpacpi_rfk_update_hwblock_state((res == TPACPI_RFK_RADIO_OFF)); + + return sysfs_emit(buf, "%d\n", + (res == TPACPI_RFK_RADIO_OFF) ? 0 : 1); +} + +static DEVICE_ATTR_RO(hotkey_radio_sw); + +static void hotkey_radio_sw_notify_change(void) +{ + if (tp_features.hotkey_wlsw) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "hotkey_radio_sw"); +} + +/* sysfs hotkey tablet mode (pollable) --------------------------------- */ +static ssize_t hotkey_tablet_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, s; + res = hotkey_get_tablet_mode(&s); + if (res < 0) + return res; + + return sysfs_emit(buf, "%d\n", !!s); +} + +static DEVICE_ATTR_RO(hotkey_tablet_mode); + +static void hotkey_tablet_mode_notify_change(void) +{ + if (tp_features.hotkey_tablet) + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "hotkey_tablet_mode"); +} + +/* sysfs wakeup reason (pollable) -------------------------------------- */ +static ssize_t hotkey_wakeup_reason_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_wakeup_reason); +} + +static DEVICE_ATTR(wakeup_reason, S_IRUGO, hotkey_wakeup_reason_show, NULL); + +static void hotkey_wakeup_reason_notify_change(void) +{ + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "wakeup_reason"); +} + +/* sysfs wakeup hotunplug_complete (pollable) -------------------------- */ +static ssize_t hotkey_wakeup_hotunplug_complete_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%d\n", hotkey_autosleep_ack); +} + +static DEVICE_ATTR(wakeup_hotunplug_complete, S_IRUGO, + hotkey_wakeup_hotunplug_complete_show, NULL); + +static void hotkey_wakeup_hotunplug_complete_notify_change(void) +{ + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, + "wakeup_hotunplug_complete"); +} + +/* sysfs adaptive kbd mode --------------------------------------------- */ + +static int adaptive_keyboard_get_mode(void); +static int adaptive_keyboard_set_mode(int new_mode); + +enum ADAPTIVE_KEY_MODE { + HOME_MODE, + WEB_BROWSER_MODE, + WEB_CONFERENCE_MODE, + FUNCTION_MODE, + LAYFLAT_MODE +}; + +static ssize_t adaptive_kbd_mode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int current_mode; + + current_mode = adaptive_keyboard_get_mode(); + if (current_mode < 0) + return current_mode; + + return sysfs_emit(buf, "%d\n", current_mode); +} + +static ssize_t adaptive_kbd_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res; + + if (parse_strtoul(buf, LAYFLAT_MODE, &t)) + return -EINVAL; + + res = adaptive_keyboard_set_mode(t); + return (res < 0) ? res : count; +} + +static DEVICE_ATTR_RW(adaptive_kbd_mode); + +static struct attribute *adaptive_kbd_attributes[] = { + &dev_attr_adaptive_kbd_mode.attr, + NULL +}; + +static umode_t hadaptive_kbd_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.has_adaptive_kbd ? attr->mode : 0; +} + +static const struct attribute_group adaptive_kbd_attr_group = { + .is_visible = hadaptive_kbd_attr_is_visible, + .attrs = adaptive_kbd_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static struct attribute *hotkey_attributes[] = { + &dev_attr_hotkey_enable.attr, + &dev_attr_hotkey_bios_enabled.attr, + &dev_attr_hotkey_bios_mask.attr, + &dev_attr_wakeup_reason.attr, + &dev_attr_wakeup_hotunplug_complete.attr, + &dev_attr_hotkey_mask.attr, + &dev_attr_hotkey_all_mask.attr, + &dev_attr_hotkey_adaptive_all_mask.attr, + &dev_attr_hotkey_recommended_mask.attr, + &dev_attr_hotkey_tablet_mode.attr, + &dev_attr_hotkey_radio_sw.attr, +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + &dev_attr_hotkey_source_mask.attr, + &dev_attr_hotkey_poll_freq.attr, +#endif + NULL +}; + +static umode_t hotkey_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &dev_attr_hotkey_tablet_mode.attr) { + if (!tp_features.hotkey_tablet) + return 0; + } else if (attr == &dev_attr_hotkey_radio_sw.attr) { + if (!tp_features.hotkey_wlsw) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group hotkey_attr_group = { + .is_visible = hotkey_attr_is_visible, + .attrs = hotkey_attributes, +}; + +/* + * Sync both the hw and sw blocking state of all switches + */ +static void tpacpi_send_radiosw_update(void) +{ + int wlsw; + + /* + * We must sync all rfkill controllers *before* issuing any + * rfkill input events, or we will race the rfkill core input + * handler. + * + * tpacpi_inputdev_send_mutex works as a synchronization point + * for the above. + * + * We optimize to avoid numerous calls to hotkey_get_wlsw. + */ + + wlsw = hotkey_get_wlsw(); + + /* Sync hw blocking state first if it is hw-blocked */ + if (wlsw == TPACPI_RFK_RADIO_OFF) + tpacpi_rfk_update_hwblock_state(true); + + /* Sync hw blocking state last if it is hw-unblocked */ + if (wlsw == TPACPI_RFK_RADIO_ON) + tpacpi_rfk_update_hwblock_state(false); + + /* Issue rfkill input event for WLSW switch */ + if (!(wlsw < 0)) { + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, + SW_RFKILL_ALL, (wlsw > 0)); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + } + + /* + * this can be unconditional, as we will poll state again + * if userspace uses the notify to read data + */ + hotkey_radio_sw_notify_change(); +} + +static void hotkey_exit(void) +{ + mutex_lock(&hotkey_mutex); + hotkey_poll_stop_sync(); + dbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_HKEY, + "restoring original HKEY status and mask\n"); + /* yes, there is a bitwise or below, we want the + * functions to be called even if one of them fail */ + if (((tp_features.hotkey_mask && + hotkey_mask_set(hotkey_orig_mask)) | + hotkey_status_set(false)) != 0) + pr_err("failed to restore hot key mask to BIOS defaults\n"); + + mutex_unlock(&hotkey_mutex); +} + +/* + * HKEY quirks: + * TPACPI_HK_Q_INIMASK: Supports FN+F3,FN+F4,FN+F12 + */ + +#define TPACPI_HK_Q_INIMASK 0x0001 + +static const struct tpacpi_quirk tpacpi_hotkey_qtable[] __initconst = { + TPACPI_Q_IBM('I', 'H', TPACPI_HK_Q_INIMASK), /* 600E */ + TPACPI_Q_IBM('I', 'N', TPACPI_HK_Q_INIMASK), /* 600E */ + TPACPI_Q_IBM('I', 'D', TPACPI_HK_Q_INIMASK), /* 770, 770E, 770ED */ + TPACPI_Q_IBM('I', 'W', TPACPI_HK_Q_INIMASK), /* A20m */ + TPACPI_Q_IBM('I', 'V', TPACPI_HK_Q_INIMASK), /* A20p */ + TPACPI_Q_IBM('1', '0', TPACPI_HK_Q_INIMASK), /* A21e, A22e */ + TPACPI_Q_IBM('K', 'U', TPACPI_HK_Q_INIMASK), /* A21e */ + TPACPI_Q_IBM('K', 'X', TPACPI_HK_Q_INIMASK), /* A21m, A22m */ + TPACPI_Q_IBM('K', 'Y', TPACPI_HK_Q_INIMASK), /* A21p, A22p */ + TPACPI_Q_IBM('1', 'B', TPACPI_HK_Q_INIMASK), /* A22e */ + TPACPI_Q_IBM('1', '3', TPACPI_HK_Q_INIMASK), /* A22m */ + TPACPI_Q_IBM('1', 'E', TPACPI_HK_Q_INIMASK), /* A30/p (0) */ + TPACPI_Q_IBM('1', 'C', TPACPI_HK_Q_INIMASK), /* R30 */ + TPACPI_Q_IBM('1', 'F', TPACPI_HK_Q_INIMASK), /* R31 */ + TPACPI_Q_IBM('I', 'Y', TPACPI_HK_Q_INIMASK), /* T20 */ + TPACPI_Q_IBM('K', 'Z', TPACPI_HK_Q_INIMASK), /* T21 */ + TPACPI_Q_IBM('1', '6', TPACPI_HK_Q_INIMASK), /* T22 */ + TPACPI_Q_IBM('I', 'Z', TPACPI_HK_Q_INIMASK), /* X20, X21 */ + TPACPI_Q_IBM('1', 'D', TPACPI_HK_Q_INIMASK), /* X22, X23, X24 */ +}; + +static int hotkey_init_tablet_mode(void) +{ + int in_tablet_mode = 0, res; + char *type = NULL; + + if (acpi_evalf(hkey_handle, &res, "GMMS", "qdd", 0)) { + int has_tablet_mode; + + in_tablet_mode = hotkey_gmms_get_tablet_mode(res, + &has_tablet_mode); + /* + * The Yoga 11e series has 2 accelerometers described by a + * BOSC0200 ACPI node. This setup relies on a Windows service + * which calls special ACPI methods on this node to report + * the laptop/tent/tablet mode to the EC. The bmc150 iio driver + * does not support this, so skip the hotkey on these models. + */ + if (has_tablet_mode && !dual_accel_detect()) + tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_GMMS; + type = "GMMS"; + } else if (acpi_evalf(hkey_handle, &res, "MHKG", "qd")) { + /* For X41t, X60t, X61t Tablets... */ + tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_MHKG; + in_tablet_mode = !!(res & TP_HOTKEY_TABLET_MASK); + type = "MHKG"; + } + + if (!tp_features.hotkey_tablet) + return 0; + + pr_info("Tablet mode switch found (type: %s), currently in %s mode\n", + type, in_tablet_mode ? "tablet" : "laptop"); + + return in_tablet_mode; +} + +static const struct key_entry keymap_ibm[] __initconst = { + /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_BATTERY } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_COFFEE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_FN_F6 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, + /* Brightness: firmware always reacts, suppressed through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, + /* Thinklight: firmware always reacts, suppressed through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, + /* + * Volume: firmware always reacts and reprograms the built-in *extra* mixer. + * Suppressed by default through hotkey_reserved_mask. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, + { KE_END } +}; + +static const struct key_entry keymap_lenovo[] __initconst = { + /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_COFFEE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_BATTERY } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_CAMERA, } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, + /* + * These should be enabled --only-- when ACPI video is disabled and + * are handled in a special way by the init code. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, + /* Suppressed by default through hotkey_reserved_mask. */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, + /* + * Volume: z60/z61, T60 (BIOS version?): firmware always reacts and + * reprograms the built-in *extra* mixer. + * T60?, T61, R60?, R61: firmware and EC tries to send these over + * the regular keyboard (not through tpacpi). There are still weird bugs + * re. MUTE. May cause the BIOS to interfere with the HDA mixer. + * Suppressed by default through hotkey_reserved_mask. + */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MICMUTE, { KEY_MICMUTE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG, { KEY_CONFIG } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_SEARCH, { KEY_SEARCH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_SCALE, { KEY_SCALE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_FILE, { KEY_FILE } }, + /* Adaptive keyboard mappings for Carbon X1 2014 translated scancodes 0x20 - 0x33 */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE2, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, { KEY_BRIGHTNESS_MIN } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLOUD, { KEY_XFER } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK9, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_VOICE, { KEY_VOICECOMMAND } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK10, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_GESTURES, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK11, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK12, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK13, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG2, { KEY_CONFIG } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_NEW_TAB, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_RELOAD, { KEY_REFRESH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BACK, { KEY_BACK } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_DOWN, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_UP, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CAMERA_MODE, { KEY_RESERVED } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, { KEY_RESERVED } }, + /* Extended hotkeys mappings translated scancodes 0x34 - 0x4d */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_STAR, { KEY_BOOKMARKS } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, { KEY_SELECTIVE_SCREENSHOT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_CALCULATOR, { KEY_CALC } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_BLUETOOTH, { KEY_BLUETOOTH } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_KEYBOARD, { KEY_KEYBOARD } }, + /* Used by "Lenovo Quick Clean" */ + { KE_KEY, TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, { KEY_FN_RIGHT_SHIFT } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, { KEY_NOTIFICATION_CENTER } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, { KEY_PICKUP_PHONE } }, + { KE_KEY, TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, { KEY_HANGUP_PHONE } }, + /* + * All mapping below are for raw untranslated hkey event codes mapped directly + * after switching to sparse keymap support. The mappings above use translated + * scancodes to preserve uAPI compatibility, see tpacpi_input_send_key(). + */ + { KE_KEY, 0x131d, { KEY_VENDOR } }, /* System debug info, similar to old ThinkPad key */ + { KE_KEY, 0x1320, { KEY_LINK_PHONE } }, + { KE_KEY, TP_HKEY_EV_TRACK_DOUBLETAP /* 0x8036 */, { KEY_PROG4 } }, + { KE_END } +}; + +static int __init hotkey_init(struct ibm_init_struct *iibm) +{ + enum keymap_index { + TPACPI_KEYMAP_IBM_GENERIC = 0, + TPACPI_KEYMAP_LENOVO_GENERIC, + }; + + static const struct tpacpi_quirk tpacpi_keymap_qtable[] __initconst = { + /* Generic maps (fallback) */ + { + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_KEYMAP_IBM_GENERIC, + }, + { + .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_KEYMAP_LENOVO_GENERIC, + }, + }; + + unsigned long keymap_id, quirks; + const struct key_entry *keymap; + bool radiosw_state = false; + bool tabletsw_state = false; + int hkeyv, res, status, camera_shutter_state; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "initializing hotkey subdriver\n"); + + BUG_ON(!tpacpi_inputdev); + BUG_ON(tpacpi_inputdev->open != NULL || + tpacpi_inputdev->close != NULL); + + TPACPI_ACPIHANDLE_INIT(hkey); + mutex_init(&hotkey_mutex); + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + mutex_init(&hotkey_thread_data_mutex); +#endif + + /* hotkey not supported on 570 */ + tp_features.hotkey = hkey_handle != NULL; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkeys are %s\n", + str_supported(tp_features.hotkey)); + + if (!tp_features.hotkey) + return -ENODEV; + + quirks = tpacpi_check_quirks(tpacpi_hotkey_qtable, + ARRAY_SIZE(tpacpi_hotkey_qtable)); + + tpacpi_disable_brightness_delay(); + + /* mask not supported on 600e/x, 770e, 770x, A21e, A2xm/p, + A30, R30, R31, T20-22, X20-21, X22-24. Detected by checking + for HKEY interface version 0x100 */ + if (acpi_evalf(hkey_handle, &hkeyv, "MHKV", "qd")) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "firmware HKEY interface version: 0x%x\n", + hkeyv); + + switch (hkeyv >> 8) { + case 1: + /* + * MHKV 0x100 in A31, R40, R40e, + * T4x, X31, and later + */ + + /* Paranoia check AND init hotkey_all_mask */ + if (!acpi_evalf(hkey_handle, &hotkey_all_mask, + "MHKA", "qd")) { + pr_err("missing MHKA handler, please report this to %s\n", + TPACPI_MAIL); + /* Fallback: pre-init for FN+F3,F4,F12 */ + hotkey_all_mask = 0x080cU; + } else { + tp_features.hotkey_mask = 1; + } + break; + + case 2: + /* + * MHKV 0x200 in X1, T460s, X260, T560, X1 Tablet (2016) + */ + + /* Paranoia check AND init hotkey_all_mask */ + if (!acpi_evalf(hkey_handle, &hotkey_all_mask, + "MHKA", "dd", 1)) { + pr_err("missing MHKA handler, please report this to %s\n", + TPACPI_MAIL); + /* Fallback: pre-init for FN+F3,F4,F12 */ + hotkey_all_mask = 0x080cU; + } else { + tp_features.hotkey_mask = 1; + } + + /* + * Check if we have an adaptive keyboard, like on the + * Lenovo Carbon X1 2014 (2nd Gen). + */ + if (acpi_evalf(hkey_handle, &hotkey_adaptive_all_mask, + "MHKA", "dd", 2)) { + if (hotkey_adaptive_all_mask != 0) + tp_features.has_adaptive_kbd = true; + } else { + tp_features.has_adaptive_kbd = false; + hotkey_adaptive_all_mask = 0x0U; + } + break; + + default: + pr_err("unknown version of the HKEY interface: 0x%x\n", + hkeyv); + pr_err("please report this to %s\n", TPACPI_MAIL); + break; + } + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkey masks are %s\n", + str_supported(tp_features.hotkey_mask)); + + /* Init hotkey_all_mask if not initialized yet */ + if (!tp_features.hotkey_mask && !hotkey_all_mask && + (quirks & TPACPI_HK_Q_INIMASK)) + hotkey_all_mask = 0x080cU; /* FN+F12, FN+F4, FN+F3 */ + + /* Init hotkey_acpi_mask and hotkey_orig_mask */ + if (tp_features.hotkey_mask) { + /* hotkey_source_mask *must* be zero for + * the first hotkey_mask_get to return hotkey_orig_mask */ + mutex_lock(&hotkey_mutex); + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + if (res) + return res; + + hotkey_orig_mask = hotkey_acpi_mask; + } else { + hotkey_orig_mask = hotkey_all_mask; + hotkey_acpi_mask = hotkey_all_mask; + } + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wlswemul) { + tp_features.hotkey_wlsw = 1; + radiosw_state = !!tpacpi_wlsw_emulstate; + pr_info("radio switch emulation enabled\n"); + } else +#endif + /* Not all thinkpads have a hardware radio switch */ + if (acpi_evalf(hkey_handle, &status, "WLSW", "qd")) { + tp_features.hotkey_wlsw = 1; + radiosw_state = !!status; + pr_info("radio switch found; radios are %s\n", str_enabled_disabled(status & BIT(0))); + } + + tabletsw_state = hotkey_init_tablet_mode(); + + /* Set up key map */ + keymap_id = tpacpi_check_quirks(tpacpi_keymap_qtable, + ARRAY_SIZE(tpacpi_keymap_qtable)); + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "using keymap number %lu\n", keymap_id); + + /* Keys which should be reserved on both IBM and Lenovo models */ + hotkey_reserved_mask = TP_ACPI_HKEY_KBD_LIGHT_MASK | + TP_ACPI_HKEY_VOLUP_MASK | + TP_ACPI_HKEY_VOLDWN_MASK | + TP_ACPI_HKEY_MUTE_MASK; + /* + * Reserve brightness up/down unconditionally on IBM models, on Lenovo + * models these are disabled based on acpi_video_get_backlight_type(). + */ + if (keymap_id == TPACPI_KEYMAP_IBM_GENERIC) { + hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK; + keymap = keymap_ibm; + } else { + keymap = keymap_lenovo; + } + + res = sparse_keymap_setup(tpacpi_inputdev, keymap, NULL); + if (res) + return res; + + camera_shutter_state = get_camera_shutter(); + if (camera_shutter_state >= 0) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_CAMERA_LENS_COVER); + input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); + } + + if (tp_features.hotkey_wlsw) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_RFKILL_ALL); + input_report_switch(tpacpi_inputdev, + SW_RFKILL_ALL, radiosw_state); + } + if (tp_features.hotkey_tablet) { + input_set_capability(tpacpi_inputdev, EV_SW, SW_TABLET_MODE); + input_report_switch(tpacpi_inputdev, + SW_TABLET_MODE, tabletsw_state); + } + + /* Do not issue duplicate brightness change events to + * userspace. tpacpi_detect_brightness_capabilities() must have + * been called before this point */ + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { + pr_info("This ThinkPad has standard ACPI backlight brightness control, supported by the ACPI video driver\n"); + pr_notice("Disabling thinkpad-acpi brightness events by default...\n"); + + /* Disable brightness up/down on Lenovo thinkpads when + * ACPI is handling them, otherwise it is plain impossible + * for userspace to do something even remotely sane */ + hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | + TP_ACPI_HKEY_BRGHTDWN_MASK; + } + +#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL + hotkey_source_mask = TPACPI_HKEY_NVRAM_GOOD_MASK + & ~hotkey_all_mask + & ~hotkey_reserved_mask; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "hotkey source mask 0x%08x, polling freq %u\n", + hotkey_source_mask, hotkey_poll_freq); +#endif + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "enabling firmware HKEY event interface...\n"); + res = hotkey_status_set(true); + if (res) { + hotkey_exit(); + return res; + } + mutex_lock(&hotkey_mutex); + res = hotkey_mask_set(((hotkey_all_mask & ~hotkey_reserved_mask) + | hotkey_driver_mask) + & ~hotkey_source_mask); + mutex_unlock(&hotkey_mutex); + if (res < 0 && res != -ENXIO) { + hotkey_exit(); + return res; + } + hotkey_user_mask = (hotkey_acpi_mask | hotkey_source_mask) + & ~hotkey_reserved_mask; + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, + "initial masks: user=0x%08x, fw=0x%08x, poll=0x%08x\n", + hotkey_user_mask, hotkey_acpi_mask, hotkey_source_mask); + + tpacpi_inputdev->open = &hotkey_inputdev_open; + tpacpi_inputdev->close = &hotkey_inputdev_close; + + hotkey_poll_setup_safe(true); + + /* Enable doubletap by default */ + tp_features.trackpoint_doubletap = 1; + + return 0; +} + +/* Thinkpad X1 Carbon support 5 modes including Home mode, Web browser + * mode, Web conference mode, Function mode and Lay-flat mode. + * We support Home mode and Function mode currently. + * + * Will consider support rest of modes in future. + * + */ +static const int adaptive_keyboard_modes[] = { + HOME_MODE, +/* WEB_BROWSER_MODE = 2, + WEB_CONFERENCE_MODE = 3, */ + FUNCTION_MODE +}; + +/* press Fn key a while second, it will switch to Function Mode. Then + * release Fn key, previous mode be restored. + */ +static bool adaptive_keyboard_mode_is_saved; +static int adaptive_keyboard_prev_mode; + +static int adaptive_keyboard_get_mode(void) +{ + int mode = 0; + + if (!acpi_evalf(hkey_handle, &mode, "GTRW", "dd", 0)) { + pr_err("Cannot read adaptive keyboard mode\n"); + return -EIO; + } + + return mode; +} + +static int adaptive_keyboard_set_mode(int new_mode) +{ + if (new_mode < 0 || + new_mode > LAYFLAT_MODE) + return -EINVAL; + + if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", new_mode)) { + pr_err("Cannot set adaptive keyboard mode\n"); + return -EIO; + } + + return 0; +} + +static int adaptive_keyboard_get_next_mode(int mode) +{ + size_t i; + size_t max_mode = ARRAY_SIZE(adaptive_keyboard_modes) - 1; + + for (i = 0; i <= max_mode; i++) { + if (adaptive_keyboard_modes[i] == mode) + break; + } + + if (i >= max_mode) + i = 0; + else + i++; + + return adaptive_keyboard_modes[i]; +} + +static void adaptive_keyboard_change_row(void) +{ + int mode; + + if (adaptive_keyboard_mode_is_saved) { + mode = adaptive_keyboard_prev_mode; + adaptive_keyboard_mode_is_saved = false; + } else { + mode = adaptive_keyboard_get_mode(); + if (mode < 0) + return; + mode = adaptive_keyboard_get_next_mode(mode); + } + + adaptive_keyboard_set_mode(mode); +} + +static void adaptive_keyboard_s_quickview_row(void) +{ + int mode; + + mode = adaptive_keyboard_get_mode(); + if (mode < 0) + return; + + adaptive_keyboard_prev_mode = mode; + adaptive_keyboard_mode_is_saved = true; + + adaptive_keyboard_set_mode(FUNCTION_MODE); +} + +/* 0x1000-0x1FFF: key presses */ +static bool hotkey_notify_hotkey(const u32 hkey, bool *send_acpi_ev) +{ + /* Never send ACPI netlink events for original hotkeys (hkey: 0x1001 - 0x1020) */ + if (hkey >= TP_HKEY_EV_ORIG_KEY_START && hkey <= TP_HKEY_EV_ORIG_KEY_END) { + *send_acpi_ev = false; + + /* Original hotkeys may be polled from NVRAM instead */ + unsigned int scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; + if (hotkey_source_mask & (1 << scancode)) + return true; + } + + return tpacpi_input_send_key(hkey, send_acpi_ev); +} + +/* 0x2000-0x2FFF: Wakeup reason */ +static bool hotkey_notify_wakeup(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_WKUP_S3_UNDOCK: /* suspend, undock */ + case TP_HKEY_EV_WKUP_S4_UNDOCK: /* hibernation, undock */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_UNDOCK; + *send_acpi_ev = false; + break; + + case TP_HKEY_EV_WKUP_S3_BAYEJ: /* suspend, bay eject */ + case TP_HKEY_EV_WKUP_S4_BAYEJ: /* hibernation, bay eject */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_BAYEJ; + *send_acpi_ev = false; + break; + + case TP_HKEY_EV_WKUP_S3_BATLOW: /* Battery on critical low level/S3 */ + case TP_HKEY_EV_WKUP_S4_BATLOW: /* Battery on critical low level/S4 */ + pr_alert("EMERGENCY WAKEUP: battery almost empty\n"); + /* how to auto-heal: */ + /* 2313: woke up from S3, go to S4/S5 */ + /* 2413: woke up from S4, go to S5 */ + break; + + default: + return false; + } + + if (hotkey_wakeup_reason != TP_ACPI_WAKEUP_NONE) { + pr_info("woke up due to a hot-unplug request...\n"); + hotkey_wakeup_reason_notify_change(); + } + return true; +} + +/* 0x4000-0x4FFF: dock-related events */ +static bool hotkey_notify_dockevent(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_UNDOCK_ACK: + /* ACPI undock operation completed after wakeup */ + hotkey_autosleep_ack = 1; + pr_info("undocked\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + return true; + + case TP_HKEY_EV_HOTPLUG_DOCK: /* docked to port replicator */ + pr_info("docked into hotplug port replicator\n"); + return true; + case TP_HKEY_EV_HOTPLUG_UNDOCK: /* undocked from port replicator */ + pr_info("undocked from hotplug port replicator\n"); + return true; + + /* + * Deliberately ignore attaching and detaching the keybord cover to avoid + * duplicates from intel-vbtn, which already emits SW_TABLET_MODE events + * to userspace. + * + * Please refer to the following thread for more information and a preliminary + * implementation using the GTOP ("Get Tablet OPtions") interface that could be + * extended to other attachment options of the ThinkPad X1 Tablet series, such as + * the Pico cartridge dock module: + * https://lore.kernel.org/platform-driver-x86/38cb8265-1e30-d547-9e12-b4ae290be737@a-kobel.de/ + */ + case TP_HKEY_EV_KBD_COVER_ATTACH: + case TP_HKEY_EV_KBD_COVER_DETACH: + *send_acpi_ev = false; + return true; + + default: + return false; + } +} + +/* 0x5000-0x5FFF: human interface helpers */ +static bool hotkey_notify_usrevent(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_PEN_INSERTED: /* X61t: tablet pen inserted into bay */ + case TP_HKEY_EV_PEN_REMOVED: /* X61t: tablet pen removed from bay */ + return true; + + case TP_HKEY_EV_TABLET_TABLET: /* X41t-X61t: tablet mode */ + case TP_HKEY_EV_TABLET_NOTEBOOK: /* X41t-X61t: normal mode */ + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_LID_CLOSE: /* Lid closed */ + case TP_HKEY_EV_LID_OPEN: /* Lid opened */ + case TP_HKEY_EV_BRGHT_CHANGED: /* brightness changed */ + /* do not propagate these events */ + *send_acpi_ev = false; + return true; + + default: + return false; + } +} + +static void thermal_dump_all_sensors(void); +static void palmsensor_refresh(void); + +/* 0x6000-0x6FFF: thermal alarms/notices and keyboard events */ +static bool hotkey_notify_6xxx(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_THM_TABLE_CHANGED: + pr_debug("EC reports: Thermal Table has changed\n"); + /* recommended action: do nothing, we don't have + * Lenovo ATM information */ + return true; + case TP_HKEY_EV_THM_CSM_COMPLETED: + pr_debug("EC reports: Thermal Control Command set completed (DYTC)\n"); + /* Thermal event - pass on to event handler */ + tpacpi_driver_event(hkey); + return true; + case TP_HKEY_EV_THM_TRANSFM_CHANGED: + pr_debug("EC reports: Thermal Transformation changed (GMTS)\n"); + /* recommended action: do nothing, we don't have + * Lenovo ATM information */ + return true; + case TP_HKEY_EV_ALARM_BAT_HOT: + pr_crit("THERMAL ALARM: battery is too hot!\n"); + /* recommended action: warn user through gui */ + break; + case TP_HKEY_EV_ALARM_BAT_XHOT: + pr_alert("THERMAL EMERGENCY: battery is extremely hot!\n"); + /* recommended action: immediate sleep/hibernate */ + break; + case TP_HKEY_EV_ALARM_BAT_LIM_CHANGE: + pr_debug("Battery Info: battery charge threshold changed\n"); + /* User changed charging threshold. No action needed */ + return true; + case TP_HKEY_EV_ALARM_SENSOR_HOT: + pr_crit("THERMAL ALARM: a sensor reports something is too hot!\n"); + /* recommended action: warn user through gui, that */ + /* some internal component is too hot */ + break; + case TP_HKEY_EV_ALARM_SENSOR_XHOT: + pr_alert("THERMAL EMERGENCY: a sensor reports something is extremely hot!\n"); + /* recommended action: immediate sleep/hibernate */ + break; + case TP_HKEY_EV_AC_CHANGED: + /* X120e, X121e, X220, X220i, X220t, X230, T420, T420s, W520: + * AC status changed; can be triggered by plugging or + * unplugging AC adapter, docking or undocking. */ + + fallthrough; + + case TP_HKEY_EV_KEY_NUMLOCK: + case TP_HKEY_EV_KEY_FN: + /* key press events, we just ignore them as long as the EC + * is still reporting them in the normal keyboard stream */ + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_KEY_FN_ESC: + /* Get the media key status to force the status LED to update */ + acpi_evalf(hkey_handle, NULL, "GMKS", "v"); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_TABLET_CHANGED: + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + *send_acpi_ev = false; + return true; + + case TP_HKEY_EV_PALM_DETECTED: + case TP_HKEY_EV_PALM_UNDETECTED: + /* palm detected - pass on to event handler */ + palmsensor_refresh(); + return true; + + default: + /* report simply as unknown, no sensor dump */ + return false; + } + + thermal_dump_all_sensors(); + return true; +} + +static bool hotkey_notify_8xxx(const u32 hkey, bool *send_acpi_ev) +{ + switch (hkey) { + case TP_HKEY_EV_TRACK_DOUBLETAP: + if (tp_features.trackpoint_doubletap) + tpacpi_input_send_key(hkey, send_acpi_ev); + + return true; + default: + return false; + } +} + +static void hotkey_notify(struct ibm_struct *ibm, u32 event) +{ + u32 hkey; + bool send_acpi_ev; + bool known_ev; + + if (event != 0x80) { + pr_err("unknown HKEY notification event %d\n", event); + /* forward it to userspace, maybe it knows how to handle it */ + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + dev_name(&ibm->acpi->device->dev), + event, 0); + return; + } + + while (1) { + if (!acpi_evalf(hkey_handle, &hkey, "MHKP", "d")) { + pr_err("failed to retrieve HKEY event\n"); + return; + } + + if (hkey == 0) { + /* queue empty */ + return; + } + + send_acpi_ev = true; + known_ev = false; + + switch (hkey >> 12) { + case 1: + /* 0x1000-0x1FFF: key presses */ + known_ev = hotkey_notify_hotkey(hkey, &send_acpi_ev); + break; + case 2: + /* 0x2000-0x2FFF: Wakeup reason */ + known_ev = hotkey_notify_wakeup(hkey, &send_acpi_ev); + break; + case 3: + /* 0x3000-0x3FFF: bay-related wakeups */ + switch (hkey) { + case TP_HKEY_EV_BAYEJ_ACK: + hotkey_autosleep_ack = 1; + pr_info("bay ejected\n"); + hotkey_wakeup_hotunplug_complete_notify_change(); + known_ev = true; + break; + case TP_HKEY_EV_OPTDRV_EJ: + /* FIXME: kick libata if SATA link offline */ + known_ev = true; + break; + } + break; + case 4: + /* 0x4000-0x4FFF: dock-related events */ + known_ev = hotkey_notify_dockevent(hkey, &send_acpi_ev); + break; + case 5: + /* 0x5000-0x5FFF: human interface helpers */ + known_ev = hotkey_notify_usrevent(hkey, &send_acpi_ev); + break; + case 6: + /* 0x6000-0x6FFF: thermal alarms/notices and + * keyboard events */ + known_ev = hotkey_notify_6xxx(hkey, &send_acpi_ev); + break; + case 7: + /* 0x7000-0x7FFF: misc */ + if (tp_features.hotkey_wlsw && + hkey == TP_HKEY_EV_RFKILL_CHANGED) { + tpacpi_send_radiosw_update(); + send_acpi_ev = false; + known_ev = true; + } + break; + case 8: + /* 0x8000-0x8FFF: misc2 */ + known_ev = hotkey_notify_8xxx(hkey, &send_acpi_ev); + break; + } + if (!known_ev) { + pr_notice("unhandled HKEY event 0x%04x\n", hkey); + pr_notice("please report the conditions when this event happened to %s\n", + TPACPI_MAIL); + } + + /* netlink events */ + if (send_acpi_ev) { + acpi_bus_generate_netlink_event( + ibm->acpi->device->pnp.device_class, + dev_name(&ibm->acpi->device->dev), + event, hkey); + } + } +} + +static void hotkey_suspend(void) +{ + /* Do these on suspend, we get the events on early resume! */ + hotkey_wakeup_reason = TP_ACPI_WAKEUP_NONE; + hotkey_autosleep_ack = 0; + + /* save previous mode of adaptive keyboard of X1 Carbon */ + if (tp_features.has_adaptive_kbd) { + if (!acpi_evalf(hkey_handle, &adaptive_keyboard_prev_mode, + "GTRW", "dd", 0)) { + pr_err("Cannot read adaptive keyboard mode.\n"); + } + } +} + +static void hotkey_resume(void) +{ + tpacpi_disable_brightness_delay(); + + mutex_lock(&hotkey_mutex); + if (hotkey_status_set(true) < 0 || + hotkey_mask_set(hotkey_acpi_mask) < 0) + pr_err("error while attempting to reset the event firmware interface\n"); + mutex_unlock(&hotkey_mutex); + + tpacpi_send_radiosw_update(); + tpacpi_input_send_tabletsw(); + hotkey_tablet_mode_notify_change(); + hotkey_wakeup_reason_notify_change(); + hotkey_wakeup_hotunplug_complete_notify_change(); + hotkey_poll_setup_safe(false); + + /* restore previous mode of adapive keyboard of X1 Carbon */ + if (tp_features.has_adaptive_kbd) { + if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", + adaptive_keyboard_prev_mode)) { + pr_err("Cannot set adaptive keyboard mode.\n"); + } + } +} + +/* procfs -------------------------------------------------------------- */ +static int hotkey_read(struct seq_file *m) +{ + int res, status; + + if (!tp_features.hotkey) { + seq_printf(m, "status:\t\tnot supported\n"); + return 0; + } + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + res = hotkey_status_get(&status); + if (!res) + res = hotkey_mask_get(); + mutex_unlock(&hotkey_mutex); + if (res) + return res; + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status & BIT(0))); + if (hotkey_all_mask) { + seq_printf(m, "mask:\t\t0x%08x\n", hotkey_user_mask); + seq_printf(m, "commands:\tenable, disable, reset, \n"); + } else { + seq_printf(m, "mask:\t\tnot supported\n"); + seq_printf(m, "commands:\tenable, disable, reset\n"); + } + + return 0; +} + +static void hotkey_enabledisable_warn(bool enable) +{ + tpacpi_log_usertask("procfs hotkey enable/disable"); + if (!WARN((tpacpi_lifecycle == TPACPI_LIFE_RUNNING || !enable), + pr_fmt("hotkey enable/disable functionality has been removed from the driver. Hotkeys are always enabled.\n"))) + pr_err("Please remove the hotkey=enable module parameter, it is deprecated. Hotkeys are always enabled.\n"); +} + +static int hotkey_write(char *buf) +{ + int res; + u32 mask; + char *cmd; + + if (!tp_features.hotkey) + return -ENODEV; + + if (mutex_lock_killable(&hotkey_mutex)) + return -ERESTARTSYS; + + mask = hotkey_user_mask; + + res = 0; + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "enable")) { + hotkey_enabledisable_warn(1); + } else if (strstarts(cmd, "disable")) { + hotkey_enabledisable_warn(0); + res = -EPERM; + } else if (strstarts(cmd, "reset")) { + mask = (hotkey_all_mask | hotkey_source_mask) + & ~hotkey_reserved_mask; + } else if (sscanf(cmd, "0x%x", &mask) == 1) { + /* mask set */ + } else if (sscanf(cmd, "%x", &mask) == 1) { + /* mask set */ + } else { + res = -EINVAL; + goto errexit; + } + } + + if (!res) { + tpacpi_disclose_usertask("procfs hotkey", + "set mask to 0x%08x\n", mask); + res = hotkey_user_mask_set(mask); + } + +errexit: + mutex_unlock(&hotkey_mutex); + return res; +} + +static const struct acpi_device_id ibm_htk_device_ids[] = { + {TPACPI_ACPI_IBM_HKEY_HID, 0}, + {TPACPI_ACPI_LENOVO_HKEY_HID, 0}, + {TPACPI_ACPI_LENOVO_HKEY_V2_HID, 0}, + {"", 0}, +}; + +static struct tp_acpi_drv_struct ibm_hotkey_acpidriver = { + .hid = ibm_htk_device_ids, + .notify = hotkey_notify, + .handle = &hkey_handle, + .type = ACPI_DEVICE_NOTIFY, +}; + +static struct ibm_struct hotkey_driver_data = { + .name = "hotkey", + .read = hotkey_read, + .write = hotkey_write, + .exit = hotkey_exit, + .resume = hotkey_resume, + .suspend = hotkey_suspend, + .acpi = &ibm_hotkey_acpidriver, +}; + +/************************************************************************* + * Bluetooth subdriver + */ + +enum { + /* ACPI GBDC/SBDC bits */ + TP_ACPI_BLUETOOTH_HWPRESENT = 0x01, /* Bluetooth hw available */ + TP_ACPI_BLUETOOTH_RADIOSSW = 0x02, /* Bluetooth radio enabled */ + TP_ACPI_BLUETOOTH_RESUMECTRL = 0x04, /* Bluetooth state at resume: + 0 = disable, 1 = enable */ +}; + +enum { + /* ACPI \BLTH commands */ + TP_ACPI_BLTH_GET_ULTRAPORT_ID = 0x00, /* Get Ultraport BT ID */ + TP_ACPI_BLTH_GET_PWR_ON_RESUME = 0x01, /* Get power-on-resume state */ + TP_ACPI_BLTH_PWR_ON_ON_RESUME = 0x02, /* Resume powered on */ + TP_ACPI_BLTH_PWR_OFF_ON_RESUME = 0x03, /* Resume powered off */ + TP_ACPI_BLTH_SAVE_STATE = 0x05, /* Save state for S4/S5 */ +}; + +#define TPACPI_RFK_BLUETOOTH_SW_NAME "tpacpi_bluetooth_sw" + +static int bluetooth_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) + return (tpacpi_bluetooth_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GBDC", "d")) + return -EIO; + + return ((status & TP_ACPI_BLUETOOTH_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int bluetooth_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s bluetooth\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) { + tpacpi_bluetooth_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_BLUETOOTH_RADIOSSW + | TP_ACPI_BLUETOOTH_RESUMECTRL; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SBDC", "vd", status)) + return -EIO; + + return 0; +} + +/* sysfs bluetooth enable ---------------------------------------------- */ +static ssize_t bluetooth_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_BLUETOOTH_SW_ID, + attr, buf); +} + +static ssize_t bluetooth_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_BLUETOOTH_SW_ID, + attr, buf, count); +} + +static DEVICE_ATTR_RW(bluetooth_enable); + +/* --------------------------------------------------------------------- */ + +static struct attribute *bluetooth_attributes[] = { + &dev_attr_bluetooth_enable.attr, + NULL +}; + +static umode_t bluetooth_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.bluetooth ? attr->mode : 0; +} + +static const struct attribute_group bluetooth_attr_group = { + .is_visible = bluetooth_attr_is_visible, + .attrs = bluetooth_attributes, +}; + +static const struct tpacpi_rfk_ops bluetooth_tprfk_ops = { + .get_status = bluetooth_get_status, + .set_status = bluetooth_set_status, +}; + +static void bluetooth_shutdown(void) +{ + /* Order firmware to save current state to NVRAM */ + if (!acpi_evalf(NULL, NULL, "\\BLTH", "vd", + TP_ACPI_BLTH_SAVE_STATE)) + pr_notice("failed to save bluetooth state to NVRAM\n"); + else + vdbg_printk(TPACPI_DBG_RFKILL, + "bluetooth state saved to NVRAM\n"); +} + +static void bluetooth_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID); + bluetooth_shutdown(); +} + +static const struct dmi_system_id fwbug_list[] __initconst = { + { + .ident = "ThinkPad E485", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20KU"), + }, + }, + { + .ident = "ThinkPad E585", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20KV"), + }, + }, + { + .ident = "ThinkPad A285 - 20MW", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MW"), + }, + }, + { + .ident = "ThinkPad A285 - 20MX", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MX"), + }, + }, + { + .ident = "ThinkPad A485 - 20MU", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MU"), + }, + }, + { + .ident = "ThinkPad A485 - 20MV", + .driver_data = &quirk_btusb_bug, + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_BOARD_NAME, "20MV"), + }, + }, + {} +}; + +static const struct pci_device_id fwbug_cards_ids[] __initconst = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24F3) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24FD) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x2526) }, + {} +}; + + +static int __init have_bt_fwbug(void) +{ + /* + * Some AMD based ThinkPads have a firmware bug that calling + * "GBDC" will cause bluetooth on Intel wireless cards blocked + */ + if (tp_features.quirks && tp_features.quirks->btusb_bug && + pci_dev_present(fwbug_cards_ids)) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + FW_BUG "disable bluetooth subdriver for Intel cards\n"); + return 1; + } else + return 0; +} + +static int __init bluetooth_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing bluetooth subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + /* bluetooth not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + G4x, R30, R31, R40e, R50e, T20-22, X20-21 */ + tp_features.bluetooth = !have_bt_fwbug() && hkey_handle && + acpi_evalf(hkey_handle, &status, "GBDC", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "bluetooth is %s, status 0x%02x\n", + str_supported(tp_features.bluetooth), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_bluetoothemul) { + tp_features.bluetooth = 1; + pr_info("bluetooth switch emulation enabled\n"); + } else +#endif + if (tp_features.bluetooth && + !(status & TP_ACPI_BLUETOOTH_HWPRESENT)) { + /* no bluetooth hardware present in system */ + tp_features.bluetooth = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "bluetooth hardware not installed\n"); + } + + if (!tp_features.bluetooth) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID, + &bluetooth_tprfk_ops, + RFKILL_TYPE_BLUETOOTH, + TPACPI_RFK_BLUETOOTH_SW_NAME, + true); + return res; +} + +/* procfs -------------------------------------------------------------- */ +static int bluetooth_read(struct seq_file *m) +{ + return tpacpi_rfk_procfs_read(TPACPI_RFK_BLUETOOTH_SW_ID, m); +} + +static int bluetooth_write(char *buf) +{ + return tpacpi_rfk_procfs_write(TPACPI_RFK_BLUETOOTH_SW_ID, buf); +} + +static struct ibm_struct bluetooth_driver_data = { + .name = "bluetooth", + .read = bluetooth_read, + .write = bluetooth_write, + .exit = bluetooth_exit, + .shutdown = bluetooth_shutdown, +}; + +/************************************************************************* + * Wan subdriver + */ + +enum { + /* ACPI GWAN/SWAN bits */ + TP_ACPI_WANCARD_HWPRESENT = 0x01, /* Wan hw available */ + TP_ACPI_WANCARD_RADIOSSW = 0x02, /* Wan radio enabled */ + TP_ACPI_WANCARD_RESUMECTRL = 0x04, /* Wan state at resume: + 0 = disable, 1 = enable */ +}; + +#define TPACPI_RFK_WWAN_SW_NAME "tpacpi_wwan_sw" + +static int wan_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) + return (tpacpi_wwan_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GWAN", "d")) + return -EIO; + + return ((status & TP_ACPI_WANCARD_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int wan_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s wwan\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) { + tpacpi_wwan_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_WANCARD_RADIOSSW + | TP_ACPI_WANCARD_RESUMECTRL; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SWAN", "vd", status)) + return -EIO; + + return 0; +} + +/* sysfs wan enable ---------------------------------------------------- */ +static ssize_t wan_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_WWAN_SW_ID, + attr, buf); +} + +static ssize_t wan_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_WWAN_SW_ID, + attr, buf, count); +} + +static DEVICE_ATTR(wwan_enable, S_IWUSR | S_IRUGO, + wan_enable_show, wan_enable_store); + +/* --------------------------------------------------------------------- */ + +static struct attribute *wan_attributes[] = { + &dev_attr_wwan_enable.attr, + NULL +}; + +static umode_t wan_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + return tp_features.wan ? attr->mode : 0; +} + +static const struct attribute_group wan_attr_group = { + .is_visible = wan_attr_is_visible, + .attrs = wan_attributes, +}; + +static const struct tpacpi_rfk_ops wan_tprfk_ops = { + .get_status = wan_get_status, + .set_status = wan_set_status, +}; + +static void wan_shutdown(void) +{ + /* Order firmware to save current state to NVRAM */ + if (!acpi_evalf(NULL, NULL, "\\WGSV", "vd", + TP_ACPI_WGSV_SAVE_STATE)) + pr_notice("failed to save WWAN state to NVRAM\n"); + else + vdbg_printk(TPACPI_DBG_RFKILL, + "WWAN state saved to NVRAM\n"); +} + +static void wan_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_WWAN_SW_ID); + wan_shutdown(); +} + +static int __init wan_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing wan subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + tp_features.wan = hkey_handle && + acpi_evalf(hkey_handle, &status, "GWAN", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "wan is %s, status 0x%02x\n", + str_supported(tp_features.wan), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_wwanemul) { + tp_features.wan = 1; + pr_info("wwan switch emulation enabled\n"); + } else +#endif + if (tp_features.wan && + !(status & TP_ACPI_WANCARD_HWPRESENT)) { + /* no wan hardware present in system */ + tp_features.wan = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "wan hardware not installed\n"); + } + + if (!tp_features.wan) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_WWAN_SW_ID, + &wan_tprfk_ops, + RFKILL_TYPE_WWAN, + TPACPI_RFK_WWAN_SW_NAME, + true); + return res; +} + +/* procfs -------------------------------------------------------------- */ +static int wan_read(struct seq_file *m) +{ + return tpacpi_rfk_procfs_read(TPACPI_RFK_WWAN_SW_ID, m); +} + +static int wan_write(char *buf) +{ + return tpacpi_rfk_procfs_write(TPACPI_RFK_WWAN_SW_ID, buf); +} + +static struct ibm_struct wan_driver_data = { + .name = "wan", + .read = wan_read, + .write = wan_write, + .exit = wan_exit, + .shutdown = wan_shutdown, +}; + +/************************************************************************* + * UWB subdriver + */ + +enum { + /* ACPI GUWB/SUWB bits */ + TP_ACPI_UWB_HWPRESENT = 0x01, /* UWB hw available */ + TP_ACPI_UWB_RADIOSSW = 0x02, /* UWB radio enabled */ +}; + +#define TPACPI_RFK_UWB_SW_NAME "tpacpi_uwb_sw" + +static int uwb_get_status(void) +{ + int status; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) + return (tpacpi_uwb_emulstate) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +#endif + + if (!acpi_evalf(hkey_handle, &status, "GUWB", "d")) + return -EIO; + + return ((status & TP_ACPI_UWB_RADIOSSW) != 0) ? + TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; +} + +static int uwb_set_status(enum tpacpi_rfkill_state state) +{ + int status; + + vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s UWB\n", + str_enable_disable(state == TPACPI_RFK_RADIO_ON)); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) { + tpacpi_uwb_emulstate = (state == TPACPI_RFK_RADIO_ON); + return 0; + } +#endif + + if (state == TPACPI_RFK_RADIO_ON) + status = TP_ACPI_UWB_RADIOSSW; + else + status = 0; + + if (!acpi_evalf(hkey_handle, NULL, "SUWB", "vd", status)) + return -EIO; + + return 0; +} + +/* --------------------------------------------------------------------- */ + +static const struct tpacpi_rfk_ops uwb_tprfk_ops = { + .get_status = uwb_get_status, + .set_status = uwb_set_status, +}; + +static void uwb_exit(void) +{ + tpacpi_destroy_rfkill(TPACPI_RFK_UWB_SW_ID); +} + +static int __init uwb_init(struct ibm_init_struct *iibm) +{ + int res; + int status = 0; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "initializing uwb subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + tp_features.uwb = hkey_handle && + acpi_evalf(hkey_handle, &status, "GUWB", "qd"); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, + "uwb is %s, status 0x%02x\n", + str_supported(tp_features.uwb), + status); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + if (dbg_uwbemul) { + tp_features.uwb = 1; + pr_info("uwb switch emulation enabled\n"); + } else +#endif + if (tp_features.uwb && + !(status & TP_ACPI_UWB_HWPRESENT)) { + /* no uwb hardware present in system */ + tp_features.uwb = 0; + dbg_printk(TPACPI_DBG_INIT, + "uwb hardware not installed\n"); + } + + if (!tp_features.uwb) + return -ENODEV; + + res = tpacpi_new_rfkill(TPACPI_RFK_UWB_SW_ID, + &uwb_tprfk_ops, + RFKILL_TYPE_UWB, + TPACPI_RFK_UWB_SW_NAME, + false); + return res; +} + +static struct ibm_struct uwb_driver_data = { + .name = "uwb", + .exit = uwb_exit, + .flags.experimental = 1, +}; + +/************************************************************************* + * Video subdriver + */ + +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + +enum video_access_mode { + TPACPI_VIDEO_NONE = 0, + TPACPI_VIDEO_570, /* 570 */ + TPACPI_VIDEO_770, /* 600e/x, 770e, 770x */ + TPACPI_VIDEO_NEW, /* all others */ +}; + +enum { /* video status flags, based on VIDEO_570 */ + TP_ACPI_VIDEO_S_LCD = 0x01, /* LCD output enabled */ + TP_ACPI_VIDEO_S_CRT = 0x02, /* CRT output enabled */ + TP_ACPI_VIDEO_S_DVI = 0x08, /* DVI output enabled */ +}; + +enum { /* TPACPI_VIDEO_570 constants */ + TP_ACPI_VIDEO_570_PHSCMD = 0x87, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHSMASK = 0x03, /* PHS bits that map to + * video_status_flags */ + TP_ACPI_VIDEO_570_PHS2CMD = 0x8b, /* unknown magic constant :( */ + TP_ACPI_VIDEO_570_PHS2SET = 0x80, /* unknown magic constant :( */ +}; + +static enum video_access_mode video_supported; +static int video_orig_autosw; + +static int video_autosw_get(void); +static int video_autosw_set(int enable); + +TPACPI_HANDLE(vid, root, + "\\_SB.PCI.AGP.VGA", /* 570 */ + "\\_SB.PCI0.AGP0.VID0", /* 600e/x, 770x */ + "\\_SB.PCI0.VID0", /* 770e */ + "\\_SB.PCI0.VID", /* A21e, G4x, R50e, X30, X40 */ + "\\_SB.PCI0.AGP.VGA", /* X100e and a few others */ + "\\_SB.PCI0.AGP.VID", /* all others */ + ); /* R30, R31 */ + +TPACPI_HANDLE(vid2, root, "\\_SB.PCI0.AGPB.VID"); /* G41 */ + +static int __init video_init(struct ibm_init_struct *iibm) +{ + int ivga; + + vdbg_printk(TPACPI_DBG_INIT, "initializing video subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(vid); + if (tpacpi_is_ibm()) + TPACPI_ACPIHANDLE_INIT(vid2); + + if (vid2_handle && acpi_evalf(NULL, &ivga, "\\IVGA", "d") && ivga) + /* G41, assume IVGA doesn't change */ + vid_handle = vid2_handle; + + if (!vid_handle) + /* video switching not supported on R30, R31 */ + video_supported = TPACPI_VIDEO_NONE; + else if (tpacpi_is_ibm() && + acpi_evalf(vid_handle, &video_orig_autosw, "SWIT", "qd")) + /* 570 */ + video_supported = TPACPI_VIDEO_570; + else if (tpacpi_is_ibm() && + acpi_evalf(vid_handle, &video_orig_autosw, "^VADL", "qd")) + /* 600e/x, 770e, 770x */ + video_supported = TPACPI_VIDEO_770; + else + /* all others */ + video_supported = TPACPI_VIDEO_NEW; + + vdbg_printk(TPACPI_DBG_INIT, "video is %s, mode %d\n", + str_supported(video_supported != TPACPI_VIDEO_NONE), + video_supported); + + return (video_supported != TPACPI_VIDEO_NONE) ? 0 : -ENODEV; +} + +static void video_exit(void) +{ + dbg_printk(TPACPI_DBG_EXIT, + "restoring original video autoswitch mode\n"); + if (video_autosw_set(video_orig_autosw)) + pr_err("error while trying to restore original video autoswitch mode\n"); +} + +static int video_outputsw_get(void) +{ + int status = 0; + int i; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(NULL, &i, "\\_SB.PHS", "dd", + TP_ACPI_VIDEO_570_PHSCMD)) + return -EIO; + status = i & TP_ACPI_VIDEO_570_PHSMASK; + break; + case TPACPI_VIDEO_770: + if (!acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + break; + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 1) || + !acpi_evalf(NULL, &i, "\\VCDC", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_CRT; + + if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0) || + !acpi_evalf(NULL, &i, "\\VCDL", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_LCD; + if (!acpi_evalf(NULL, &i, "\\VCDD", "d")) + return -EIO; + if (i) + status |= TP_ACPI_VIDEO_S_DVI; + break; + default: + return -ENOSYS; + } + + return status; +} + +static int video_outputsw_set(int status) +{ + int autosw; + int res = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = acpi_evalf(NULL, NULL, + "\\_SB.PHS2", "vdd", + TP_ACPI_VIDEO_570_PHS2CMD, + status | TP_ACPI_VIDEO_570_PHS2SET); + break; + case TPACPI_VIDEO_770: + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, + "ASWT", "vdd", status * 0x100, 0); + if (!autosw && video_autosw_set(autosw)) { + pr_err("video auto-switch left enabled due to error\n"); + return -EIO; + } + break; + case TPACPI_VIDEO_NEW: + res = acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0x80) && + acpi_evalf(NULL, NULL, "\\VSDS", "vdd", status, 1); + break; + default: + return -ENOSYS; + } + + return (res) ? 0 : -EIO; +} + +static int video_autosw_get(void) +{ + int autosw = 0; + + switch (video_supported) { + case TPACPI_VIDEO_570: + if (!acpi_evalf(vid_handle, &autosw, "SWIT", "d")) + return -EIO; + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + if (!acpi_evalf(vid_handle, &autosw, "^VDEE", "d")) + return -EIO; + break; + default: + return -ENOSYS; + } + + return autosw & 1; +} + +static int video_autosw_set(int enable) +{ + if (!acpi_evalf(vid_handle, NULL, "_DOS", "vd", (enable) ? 1 : 0)) + return -EIO; + return 0; +} + +static int video_outputsw_cycle(void) +{ + int autosw = video_autosw_get(); + int res; + + if (autosw < 0) + return autosw; + + switch (video_supported) { + case TPACPI_VIDEO_570: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(ec_handle, NULL, "_Q16", "v"); + break; + case TPACPI_VIDEO_770: + case TPACPI_VIDEO_NEW: + res = video_autosw_set(1); + if (res) + return res; + res = acpi_evalf(vid_handle, NULL, "VSWT", "v"); + break; + default: + return -ENOSYS; + } + if (!autosw && video_autosw_set(autosw)) { + pr_err("video auto-switch left enabled due to error\n"); + return -EIO; + } + + return (res) ? 0 : -EIO; +} + +static int video_expand_toggle(void) +{ + switch (video_supported) { + case TPACPI_VIDEO_570: + return acpi_evalf(ec_handle, NULL, "_Q17", "v") ? + 0 : -EIO; + case TPACPI_VIDEO_770: + return acpi_evalf(vid_handle, NULL, "VEXP", "v") ? + 0 : -EIO; + case TPACPI_VIDEO_NEW: + return acpi_evalf(NULL, NULL, "\\VEXP", "v") ? + 0 : -EIO; + default: + return -ENOSYS; + } + /* not reached */ +} + +static int video_read(struct seq_file *m) +{ + int status, autosw; + + if (video_supported == TPACPI_VIDEO_NONE) { + seq_printf(m, "status:\t\tnot supported\n"); + return 0; + } + + /* Even reads can crash X.org, so... */ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + status = video_outputsw_get(); + if (status < 0) + return status; + + autosw = video_autosw_get(); + if (autosw < 0) + return autosw; + + seq_printf(m, "status:\t\tsupported\n"); + seq_printf(m, "lcd:\t\t%s\n", str_enabled_disabled(status & BIT(0))); + seq_printf(m, "crt:\t\t%s\n", str_enabled_disabled(status & BIT(1))); + if (video_supported == TPACPI_VIDEO_NEW) + seq_printf(m, "dvi:\t\t%s\n", str_enabled_disabled(status & BIT(3))); + seq_printf(m, "auto:\t\t%s\n", str_enabled_disabled(autosw & BIT(0))); + seq_printf(m, "commands:\tlcd_enable, lcd_disable\n"); + seq_printf(m, "commands:\tcrt_enable, crt_disable\n"); + if (video_supported == TPACPI_VIDEO_NEW) + seq_printf(m, "commands:\tdvi_enable, dvi_disable\n"); + seq_printf(m, "commands:\tauto_enable, auto_disable\n"); + seq_printf(m, "commands:\tvideo_switch, expand_toggle\n"); + + return 0; +} + +static int video_write(char *buf) +{ + char *cmd; + int enable, disable, status; + int res; + + if (video_supported == TPACPI_VIDEO_NONE) + return -ENODEV; + + /* Even reads can crash X.org, let alone writes... */ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + enable = 0; + disable = 0; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "lcd_enable")) { + enable |= TP_ACPI_VIDEO_S_LCD; + } else if (strstarts(cmd, "lcd_disable")) { + disable |= TP_ACPI_VIDEO_S_LCD; + } else if (strstarts(cmd, "crt_enable")) { + enable |= TP_ACPI_VIDEO_S_CRT; + } else if (strstarts(cmd, "crt_disable")) { + disable |= TP_ACPI_VIDEO_S_CRT; + } else if (video_supported == TPACPI_VIDEO_NEW && + strstarts(cmd, "dvi_enable")) { + enable |= TP_ACPI_VIDEO_S_DVI; + } else if (video_supported == TPACPI_VIDEO_NEW && + strstarts(cmd, "dvi_disable")) { + disable |= TP_ACPI_VIDEO_S_DVI; + } else if (strstarts(cmd, "auto_enable")) { + res = video_autosw_set(1); + if (res) + return res; + } else if (strstarts(cmd, "auto_disable")) { + res = video_autosw_set(0); + if (res) + return res; + } else if (strstarts(cmd, "video_switch")) { + res = video_outputsw_cycle(); + if (res) + return res; + } else if (strstarts(cmd, "expand_toggle")) { + res = video_expand_toggle(); + if (res) + return res; + } else + return -EINVAL; + } + + if (enable || disable) { + status = video_outputsw_get(); + if (status < 0) + return status; + res = video_outputsw_set((status & ~disable) | enable); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct video_driver_data = { + .name = "video", + .read = video_read, + .write = video_write, + .exit = video_exit, +}; + +#endif /* CONFIG_THINKPAD_ACPI_VIDEO */ + +/************************************************************************* + * Keyboard backlight subdriver + */ + +static enum led_brightness kbdlight_brightness; +static DEFINE_MUTEX(kbdlight_mutex); + +static int kbdlight_set_level(int level) +{ + int ret = 0; + + if (!hkey_handle) + return -ENXIO; + + mutex_lock(&kbdlight_mutex); + + if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level)) + ret = -EIO; + else + kbdlight_brightness = level; + + mutex_unlock(&kbdlight_mutex); + + return ret; +} + +static int kbdlight_get_level(void) +{ + int status = 0; + + if (!hkey_handle) + return -ENXIO; + + if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0)) + return -EIO; + + if (status < 0) + return status; + + return status & 0x3; +} + +static bool kbdlight_is_supported(void) +{ + int status = 0; + + if (!hkey_handle) + return false; + + if (!acpi_has_method(hkey_handle, "MLCG")) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n"); + return false; + } + + if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n"); + return false; + } + + if (status < 0) { + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status); + return false; + } + + vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status); + /* + * Guessed test for keyboard backlight: + * + * Machines with backlight keyboard return: + * b010100000010000000XX - ThinkPad X1 Carbon 3rd + * b110100010010000000XX - ThinkPad x230 + * b010100000010000000XX - ThinkPad x240 + * b010100000010000000XX - ThinkPad W541 + * (XX is current backlight level) + * + * Machines without backlight keyboard return: + * b10100001000000000000 - ThinkPad x230 + * b10110001000000000000 - ThinkPad E430 + * b00000000000000000000 - ThinkPad E450 + * + * Candidate BITs for detection test (XOR): + * b01000000001000000000 + * ^ + */ + return status & BIT(9); +} + +static int kbdlight_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return kbdlight_set_level(brightness); +} + +static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev) +{ + int level; + + level = kbdlight_get_level(); + if (level < 0) + return 0; + + return level; +} + +static struct tpacpi_led_classdev tpacpi_led_kbdlight = { + .led_classdev = { + .name = "tpacpi::kbd_backlight", + .max_brightness = 2, + .flags = LED_BRIGHT_HW_CHANGED, + .brightness_set_blocking = &kbdlight_sysfs_set, + .brightness_get = &kbdlight_sysfs_get, + } +}; + +static int __init kbdlight_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(hkey); + + if (!kbdlight_is_supported()) { + tp_features.kbdlight = 0; + vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n"); + return -ENODEV; + } + + kbdlight_brightness = kbdlight_sysfs_get(NULL); + tp_features.kbdlight = 1; + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_led_kbdlight.led_classdev); + if (rc < 0) { + tp_features.kbdlight = 0; + return rc; + } + + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask | + TP_ACPI_HKEY_KBD_LIGHT_MASK); + return 0; +} + +static void kbdlight_exit(void) +{ + led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev); +} + +static int kbdlight_set_level_and_update(int level) +{ + int ret; + struct led_classdev *led_cdev; + + ret = kbdlight_set_level(level); + led_cdev = &tpacpi_led_kbdlight.led_classdev; + + if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED)) + led_cdev->brightness = level; + + return ret; +} + +static int kbdlight_read(struct seq_file *m) +{ + int level; + + if (!tp_features.kbdlight) { + seq_printf(m, "status:\t\tnot supported\n"); + } else { + level = kbdlight_get_level(); + if (level < 0) + seq_printf(m, "status:\t\terror %d\n", level); + else + seq_printf(m, "status:\t\t%d\n", level); + seq_printf(m, "commands:\t0, 1, 2\n"); + } + + return 0; +} + +static int kbdlight_write(char *buf) +{ + char *cmd; + int res, level = -EINVAL; + + if (!tp_features.kbdlight) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + res = kstrtoint(cmd, 10, &level); + if (res < 0) + return res; + } + + if (level >= 3 || level < 0) + return -EINVAL; + + return kbdlight_set_level_and_update(level); +} + +static void kbdlight_suspend(void) +{ + struct led_classdev *led_cdev; + + if (!tp_features.kbdlight) + return; + + led_cdev = &tpacpi_led_kbdlight.led_classdev; + led_update_brightness(led_cdev); + led_classdev_suspend(led_cdev); +} + +static void kbdlight_resume(void) +{ + if (!tp_features.kbdlight) + return; + + led_classdev_resume(&tpacpi_led_kbdlight.led_classdev); +} + +static struct ibm_struct kbdlight_driver_data = { + .name = "kbdlight", + .read = kbdlight_read, + .write = kbdlight_write, + .suspend = kbdlight_suspend, + .resume = kbdlight_resume, + .exit = kbdlight_exit, +}; + +/************************************************************************* + * Light (thinklight) subdriver + */ + +TPACPI_HANDLE(lght, root, "\\LGHT"); /* A21e, A2xm/p, T20-22, X20-21 */ +TPACPI_HANDLE(ledb, ec, "LEDB"); /* G4x */ + +static int light_get_status(void) +{ + int status = 0; + + if (tp_features.light_status) { + if (!acpi_evalf(ec_handle, &status, "KBLT", "d")) + return -EIO; + return (!!status); + } + + return -ENXIO; +} + +static int light_set_status(int status) +{ + int rc; + + if (tp_features.light) { + if (cmos_handle) { + rc = acpi_evalf(cmos_handle, NULL, NULL, "vd", + (status) ? + TP_CMOS_THINKLIGHT_ON : + TP_CMOS_THINKLIGHT_OFF); + } else { + rc = acpi_evalf(lght_handle, NULL, NULL, "vd", + (status) ? 1 : 0); + } + return (rc) ? 0 : -EIO; + } + + return -ENXIO; +} + +static int light_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return light_set_status((brightness != LED_OFF) ? + TPACPI_LED_ON : TPACPI_LED_OFF); +} + +static enum led_brightness light_sysfs_get(struct led_classdev *led_cdev) +{ + return (light_get_status() == 1) ? LED_ON : LED_OFF; +} + +static struct tpacpi_led_classdev tpacpi_led_thinklight = { + .led_classdev = { + .name = "tpacpi::thinklight", + .max_brightness = 1, + .brightness_set_blocking = &light_sysfs_set, + .brightness_get = &light_sysfs_get, + } +}; + +static int __init light_init(struct ibm_init_struct *iibm) +{ + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing light subdriver\n"); + + if (tpacpi_is_ibm()) { + TPACPI_ACPIHANDLE_INIT(ledb); + TPACPI_ACPIHANDLE_INIT(lght); + } + TPACPI_ACPIHANDLE_INIT(cmos); + + /* light not supported on 570, 600e/x, 770e, 770x, G4x, R30, R31 */ + tp_features.light = (cmos_handle || lght_handle) && !ledb_handle; + + if (tp_features.light) + /* light status not supported on + 570, 600e/x, 770e, 770x, G4x, R30, R31, R32, X20 */ + tp_features.light_status = + acpi_evalf(ec_handle, NULL, "KBLT", "qv"); + + vdbg_printk(TPACPI_DBG_INIT, "light is %s, light status is %s\n", + str_supported(tp_features.light), + str_supported(tp_features.light_status)); + + if (!tp_features.light) + return -ENODEV; + + rc = led_classdev_register(&tpacpi_pdev->dev, + &tpacpi_led_thinklight.led_classdev); + + if (rc < 0) { + tp_features.light = 0; + tp_features.light_status = 0; + } else { + rc = 0; + } + + return rc; +} + +static void light_exit(void) +{ + led_classdev_unregister(&tpacpi_led_thinklight.led_classdev); +} + +static int light_read(struct seq_file *m) +{ + int status; + + if (!tp_features.light) { + seq_printf(m, "status:\t\tnot supported\n"); + } else if (!tp_features.light_status) { + seq_printf(m, "status:\t\tunknown\n"); + seq_printf(m, "commands:\ton, off\n"); + } else { + status = light_get_status(); + if (status < 0) + return status; + seq_printf(m, "status:\t\t%s\n", str_on_off(status & BIT(0))); + seq_printf(m, "commands:\ton, off\n"); + } + + return 0; +} + +static int light_write(char *buf) +{ + char *cmd; + int newstatus = 0; + + if (!tp_features.light) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "on")) { + newstatus = 1; + } else if (strstarts(cmd, "off")) { + newstatus = 0; + } else + return -EINVAL; + } + + return light_set_status(newstatus); +} + +static struct ibm_struct light_driver_data = { + .name = "light", + .read = light_read, + .write = light_write, + .exit = light_exit, +}; + +/************************************************************************* + * CMOS subdriver + */ + +/* sysfs cmos_command -------------------------------------------------- */ +static ssize_t cmos_command_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long cmos_cmd; + int res; + + if (parse_strtoul(buf, 21, &cmos_cmd)) + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + return (res) ? res : count; +} + +static DEVICE_ATTR_WO(cmos_command); + +static struct attribute *cmos_attributes[] = { + &dev_attr_cmos_command.attr, + NULL +}; + +static umode_t cmos_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return cmos_handle ? attr->mode : 0; +} + +static const struct attribute_group cmos_attr_group = { + .is_visible = cmos_attr_is_visible, + .attrs = cmos_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static int __init cmos_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, + "initializing cmos commands subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(cmos); + + vdbg_printk(TPACPI_DBG_INIT, "cmos commands are %s\n", + str_supported(cmos_handle != NULL)); + + return cmos_handle ? 0 : -ENODEV; +} + +static int cmos_read(struct seq_file *m) +{ + /* cmos not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, + R30, R31, T20-22, X20-21 */ + if (!cmos_handle) + seq_printf(m, "status:\t\tnot supported\n"); + else { + seq_printf(m, "status:\t\tsupported\n"); + seq_printf(m, "commands:\t ( is 0-21)\n"); + } + + return 0; +} + +static int cmos_write(char *buf) +{ + char *cmd; + int cmos_cmd, res; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%u", &cmos_cmd) == 1 && + cmos_cmd >= 0 && cmos_cmd <= 21) { + /* cmos_cmd set */ + } else + return -EINVAL; + + res = issue_thinkpad_cmos_command(cmos_cmd); + if (res) + return res; + } + + return 0; +} + +static struct ibm_struct cmos_driver_data = { + .name = "cmos", + .read = cmos_read, + .write = cmos_write, +}; + +/************************************************************************* + * LED subdriver + */ + +enum led_access_mode { + TPACPI_LED_NONE = 0, + TPACPI_LED_570, /* 570 */ + TPACPI_LED_OLD, /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + TPACPI_LED_NEW, /* all others */ +}; + +enum { /* For TPACPI_LED_OLD */ + TPACPI_LED_EC_HLCL = 0x0c, /* EC reg to get led to power on */ + TPACPI_LED_EC_HLBL = 0x0d, /* EC reg to blink a lit led */ + TPACPI_LED_EC_HLMS = 0x0e, /* EC reg to select led to command */ +}; + +static enum led_access_mode led_supported; + +static acpi_handle led_handle; + +#define TPACPI_LED_NUMLEDS 16 +static struct tpacpi_led_classdev *tpacpi_leds; +static enum led_status_t tpacpi_led_state_cache[TPACPI_LED_NUMLEDS]; +static const char * const tpacpi_led_names[TPACPI_LED_NUMLEDS] = { + /* there's a limit of 19 chars + NULL before 2.6.26 */ + "tpacpi::power", + "tpacpi:orange:batt", + "tpacpi:green:batt", + "tpacpi::dock_active", + "tpacpi::bay_active", + "tpacpi::dock_batt", + "tpacpi::unknown_led", + "tpacpi::standby", + "tpacpi::dock_status1", + "tpacpi::dock_status2", + "tpacpi::lid_logo_dot", + "tpacpi::unknown_led3", + "tpacpi::thinkvantage", +}; +#define TPACPI_SAFE_LEDS 0x1481U + +static inline bool tpacpi_is_led_restricted(const unsigned int led) +{ +#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS + return false; +#else + return (1U & (TPACPI_SAFE_LEDS >> led)) == 0; +#endif +} + +static int led_get_status(const unsigned int led) +{ + int status; + enum led_status_t led_s; + + switch (led_supported) { + case TPACPI_LED_570: + if (!acpi_evalf(ec_handle, + &status, "GLED", "dd", 1 << led)) + return -EIO; + led_s = (status == 0) ? + TPACPI_LED_OFF : + ((status == 1) ? + TPACPI_LED_ON : + TPACPI_LED_BLINK); + tpacpi_led_state_cache[led] = led_s; + return led_s; + default: + return -ENXIO; + } + + /* not reached */ +} + +static int led_set_status(const unsigned int led, + const enum led_status_t ledstatus) +{ + /* off, on, blink. Index is led_status_t */ + static const unsigned int led_sled_arg1[] = { 0, 1, 3 }; + static const unsigned int led_led_arg1[] = { 0, 0x80, 0xc0 }; + + int rc = 0; + + switch (led_supported) { + case TPACPI_LED_570: + /* 570 */ + if (unlikely(led > 7)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + (1 << led), led_sled_arg1[ledstatus])) + return -EIO; + break; + case TPACPI_LED_OLD: + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20 */ + if (unlikely(led > 7)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + rc = ec_write(TPACPI_LED_EC_HLMS, (1 << led)); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLBL, + (ledstatus == TPACPI_LED_BLINK) << led); + if (rc >= 0) + rc = ec_write(TPACPI_LED_EC_HLCL, + (ledstatus != TPACPI_LED_OFF) << led); + break; + case TPACPI_LED_NEW: + /* all others */ + if (unlikely(led >= TPACPI_LED_NUMLEDS)) + return -EINVAL; + if (unlikely(tpacpi_is_led_restricted(led))) + return -EPERM; + if (!acpi_evalf(led_handle, NULL, NULL, "vdd", + led, led_led_arg1[ledstatus])) + return -EIO; + break; + default: + return -ENXIO; + } + + if (!rc) + tpacpi_led_state_cache[led] = ledstatus; + + return rc; +} + +static int led_sysfs_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + enum led_status_t new_state; + + if (brightness == LED_OFF) + new_state = TPACPI_LED_OFF; + else if (tpacpi_led_state_cache[data->led] != TPACPI_LED_BLINK) + new_state = TPACPI_LED_ON; + else + new_state = TPACPI_LED_BLINK; + + return led_set_status(data->led, new_state); +} + +static int led_sysfs_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + /* Can we choose the flash rate? */ + if (*delay_on == 0 && *delay_off == 0) { + /* yes. set them to the hardware blink rate (1 Hz) */ + *delay_on = 500; /* ms */ + *delay_off = 500; /* ms */ + } else if ((*delay_on != 500) || (*delay_off != 500)) + return -EINVAL; + + return led_set_status(data->led, TPACPI_LED_BLINK); +} + +static enum led_brightness led_sysfs_get(struct led_classdev *led_cdev) +{ + int rc; + + struct tpacpi_led_classdev *data = container_of(led_cdev, + struct tpacpi_led_classdev, led_classdev); + + rc = led_get_status(data->led); + + if (rc == TPACPI_LED_OFF || rc < 0) + rc = LED_OFF; /* no error handling in led class :( */ + else + rc = LED_FULL; + + return rc; +} + +static void led_exit(void) +{ + unsigned int i; + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) + led_classdev_unregister(&tpacpi_leds[i].led_classdev); + + kfree(tpacpi_leds); +} + +static int __init tpacpi_init_led(unsigned int led) +{ + /* LEDs with no name don't get registered */ + if (!tpacpi_led_names[led]) + return 0; + + tpacpi_leds[led].led_classdev.brightness_set_blocking = &led_sysfs_set; + tpacpi_leds[led].led_classdev.blink_set = &led_sysfs_blink_set; + if (led_supported == TPACPI_LED_570) + tpacpi_leds[led].led_classdev.brightness_get = &led_sysfs_get; + + tpacpi_leds[led].led_classdev.name = tpacpi_led_names[led]; + tpacpi_leds[led].led_classdev.flags = LED_RETAIN_AT_SHUTDOWN; + tpacpi_leds[led].led = led; + + return led_classdev_register(&tpacpi_pdev->dev, &tpacpi_leds[led].led_classdev); +} + +static const struct tpacpi_quirk led_useful_qtable[] __initconst = { + TPACPI_Q_IBM('1', 'E', 0x009f), /* A30 */ + TPACPI_Q_IBM('1', 'N', 0x009f), /* A31 */ + TPACPI_Q_IBM('1', 'G', 0x009f), /* A31 */ + + TPACPI_Q_IBM('1', 'I', 0x0097), /* T30 */ + TPACPI_Q_IBM('1', 'R', 0x0097), /* T40, T41, T42, R50, R51 */ + TPACPI_Q_IBM('7', '0', 0x0097), /* T43, R52 */ + TPACPI_Q_IBM('1', 'Y', 0x0097), /* T43 */ + TPACPI_Q_IBM('1', 'W', 0x0097), /* R50e */ + TPACPI_Q_IBM('1', 'V', 0x0097), /* R51 */ + TPACPI_Q_IBM('7', '8', 0x0097), /* R51e */ + TPACPI_Q_IBM('7', '6', 0x0097), /* R52 */ + + TPACPI_Q_IBM('1', 'K', 0x00bf), /* X30 */ + TPACPI_Q_IBM('1', 'Q', 0x00bf), /* X31, X32 */ + TPACPI_Q_IBM('1', 'U', 0x00bf), /* X40 */ + TPACPI_Q_IBM('7', '4', 0x00bf), /* X41 */ + TPACPI_Q_IBM('7', '5', 0x00bf), /* X41t */ + + TPACPI_Q_IBM('7', '9', 0x1f97), /* T60 (1) */ + TPACPI_Q_IBM('7', '7', 0x1f97), /* Z60* (1) */ + TPACPI_Q_IBM('7', 'F', 0x1f97), /* Z61* (1) */ + TPACPI_Q_IBM('7', 'B', 0x1fb7), /* X60 (1) */ + + /* (1) - may have excess leds enabled on MSB */ + + /* Defaults (order matters, keep last, don't reorder!) */ + { /* Lenovo */ + .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = 0x1fffU, + }, + { /* IBM ThinkPads with no EC version string */ + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_UNKNOWN, + .quirks = 0x00ffU, + }, + { /* IBM ThinkPads with EC version string */ + .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, + .quirks = 0x00bfU, + }, +}; + +static enum led_access_mode __init led_init_detect_mode(void) +{ + acpi_status status; + + if (tpacpi_is_ibm()) { + /* 570 */ + status = acpi_get_handle(ec_handle, "SLED", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_570; + + /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ + status = acpi_get_handle(ec_handle, "SYSL", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_OLD; + } + + /* most others */ + status = acpi_get_handle(ec_handle, "LED", &led_handle); + if (ACPI_SUCCESS(status)) + return TPACPI_LED_NEW; + + /* R30, R31, and unknown firmwares */ + led_handle = NULL; + return TPACPI_LED_NONE; +} + +static int __init led_init(struct ibm_init_struct *iibm) +{ + unsigned int i; + int rc; + unsigned long useful_leds; + + vdbg_printk(TPACPI_DBG_INIT, "initializing LED subdriver\n"); + + led_supported = led_init_detect_mode(); + + if (led_supported != TPACPI_LED_NONE) { + useful_leds = tpacpi_check_quirks(led_useful_qtable, + ARRAY_SIZE(led_useful_qtable)); + + if (!useful_leds) { + led_handle = NULL; + led_supported = TPACPI_LED_NONE; + } + } + + vdbg_printk(TPACPI_DBG_INIT, "LED commands are %s, mode %d\n", + str_supported(led_supported), led_supported); + + if (led_supported == TPACPI_LED_NONE) + return -ENODEV; + + tpacpi_leds = kcalloc(TPACPI_LED_NUMLEDS, sizeof(*tpacpi_leds), + GFP_KERNEL); + if (!tpacpi_leds) { + pr_err("Out of memory for LED data\n"); + return -ENOMEM; + } + + for (i = 0; i < TPACPI_LED_NUMLEDS; i++) { + tpacpi_leds[i].led = -1; + + if (!tpacpi_is_led_restricted(i) && test_bit(i, &useful_leds)) { + rc = tpacpi_init_led(i); + if (rc < 0) { + led_exit(); + return rc; + } + } + } + +#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS + pr_notice("warning: userspace override of important firmware LEDs is enabled\n"); +#endif + return 0; +} + +#define str_led_status(s) ((s) >= TPACPI_LED_BLINK ? "blinking" : str_on_off(s)) + +static int led_read(struct seq_file *m) +{ + if (!led_supported) { + seq_printf(m, "status:\t\tnot supported\n"); + return 0; + } + seq_printf(m, "status:\t\tsupported\n"); + + if (led_supported == TPACPI_LED_570) { + /* 570 */ + int i, status; + for (i = 0; i < 8; i++) { + status = led_get_status(i); + if (status < 0) + return -EIO; + seq_printf(m, "%d:\t\t%s\n", i, str_led_status(status)); + } + } + + seq_printf(m, "commands:\t on, off, blink ( is 0-15)\n"); + + return 0; +} + +static int led_write(char *buf) +{ + char *cmd; + int led, rc; + enum led_status_t s; + + if (!led_supported) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%d", &led) != 1) + return -EINVAL; + + if (led < 0 || led > (TPACPI_LED_NUMLEDS - 1)) + return -ENODEV; + + if (tpacpi_leds[led].led < 0) + return -ENODEV; + + if (strstr(cmd, "off")) { + s = TPACPI_LED_OFF; + } else if (strstr(cmd, "on")) { + s = TPACPI_LED_ON; + } else if (strstr(cmd, "blink")) { + s = TPACPI_LED_BLINK; + } else { + return -EINVAL; + } + + rc = led_set_status(led, s); + if (rc < 0) + return rc; + } + + return 0; +} + +static struct ibm_struct led_driver_data = { + .name = "led", + .read = led_read, + .write = led_write, + .exit = led_exit, +}; + +/************************************************************************* + * Beep subdriver + */ + +TPACPI_HANDLE(beep, ec, "BEEP"); /* all except R30, R31 */ + +#define TPACPI_BEEP_Q1 0x0001 + +static const struct tpacpi_quirk beep_quirk_table[] __initconst = { + TPACPI_Q_IBM('I', 'M', TPACPI_BEEP_Q1), /* 570 */ + TPACPI_Q_IBM('I', 'U', TPACPI_BEEP_Q1), /* 570E - unverified */ +}; + +static int __init beep_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT, "initializing beep subdriver\n"); + + TPACPI_ACPIHANDLE_INIT(beep); + + vdbg_printk(TPACPI_DBG_INIT, "beep is %s\n", + str_supported(beep_handle != NULL)); + + quirks = tpacpi_check_quirks(beep_quirk_table, + ARRAY_SIZE(beep_quirk_table)); + + tp_features.beep_needs_two_args = !!(quirks & TPACPI_BEEP_Q1); + + return (beep_handle) ? 0 : -ENODEV; +} + +static int beep_read(struct seq_file *m) +{ + if (!beep_handle) + seq_printf(m, "status:\t\tnot supported\n"); + else { + seq_printf(m, "status:\t\tsupported\n"); + seq_printf(m, "commands:\t ( is 0-17)\n"); + } + + return 0; +} + +static int beep_write(char *buf) +{ + char *cmd; + int beep_cmd; + + if (!beep_handle) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + if (sscanf(cmd, "%u", &beep_cmd) == 1 && + beep_cmd >= 0 && beep_cmd <= 17) { + /* beep_cmd set */ + } else + return -EINVAL; + if (tp_features.beep_needs_two_args) { + if (!acpi_evalf(beep_handle, NULL, NULL, "vdd", + beep_cmd, 0)) + return -EIO; + } else { + if (!acpi_evalf(beep_handle, NULL, NULL, "vd", + beep_cmd)) + return -EIO; + } + } + + return 0; +} + +static struct ibm_struct beep_driver_data = { + .name = "beep", + .read = beep_read, + .write = beep_write, +}; + +/************************************************************************* + * Thermal subdriver + */ + +enum thermal_access_mode { + TPACPI_THERMAL_NONE = 0, /* No thermal support */ + TPACPI_THERMAL_ACPI_TMP07, /* Use ACPI TMP0-7 */ + TPACPI_THERMAL_ACPI_UPDT, /* Use ACPI TMP0-7 with UPDT */ + TPACPI_THERMAL_TPEC_8, /* Use ACPI EC regs, 8 sensors */ + TPACPI_THERMAL_TPEC_12, /* Use ACPI EC regs, 12 sensors */ + TPACPI_THERMAL_TPEC_16, /* Use ACPI EC regs, 16 sensors */ +}; + +enum { /* TPACPI_THERMAL_TPEC_* */ + TP_EC_THERMAL_TMP0 = 0x78, /* ACPI EC regs TMP 0..7 */ + TP_EC_THERMAL_TMP8 = 0xC0, /* ACPI EC regs TMP 8..15 */ + TP_EC_THERMAL_TMP0_NS = 0xA8, /* ACPI EC Non-Standard regs TMP 0..7 */ + TP_EC_THERMAL_TMP8_NS = 0xB8, /* ACPI EC Non-standard regs TMP 8..11 */ + TP_EC_FUNCREV = 0xEF, /* ACPI EC Functional revision */ + TP_EC_THERMAL_TMP_NA = -128, /* ACPI EC sensor not available */ + + TPACPI_THERMAL_SENSOR_NA = -128000, /* Sensor not available */ +}; + + +#define TPACPI_MAX_THERMAL_SENSORS 16 /* Max thermal sensors supported */ +struct ibm_thermal_sensors_struct { + s32 temp[TPACPI_MAX_THERMAL_SENSORS]; +}; + +static const struct tpacpi_quirk thermal_quirk_table[] __initconst = { + /* Non-standard address for thermal registers on some ThinkPads */ + TPACPI_Q_LNV3('R', '1', 'F', true), /* L13 Yoga Gen 2 */ + TPACPI_Q_LNV3('N', '2', 'U', true), /* X13 Yoga Gen 2*/ + TPACPI_Q_LNV3('R', '0', 'R', true), /* L380 */ + TPACPI_Q_LNV3('R', '1', '5', true), /* L13 Yoga Gen 1*/ + TPACPI_Q_LNV3('R', '1', '0', true), /* L390 */ + TPACPI_Q_LNV3('N', '2', 'L', true), /* X13 Yoga Gen 1*/ + TPACPI_Q_LNV3('R', '0', 'T', true), /* 11e Gen5 GL*/ + TPACPI_Q_LNV3('R', '1', 'D', true), /* 11e Gen5 GL-R*/ + TPACPI_Q_LNV3('R', '0', 'V', true), /* 11e Gen5 KL-Y*/ +}; + +static enum thermal_access_mode thermal_read_mode; +static bool thermal_use_labels; +static bool thermal_with_ns_address; /* Non-standard thermal reg address */ + +/* Function to check thermal read mode */ +static enum thermal_access_mode __init thermal_read_mode_check(void) +{ + u8 t, ta1, ta2, ver = 0; + int i; + int acpi_tmp7; + + acpi_tmp7 = acpi_evalf(ec_handle, NULL, "TMP7", "qv"); + + if (thinkpad_id.ec_model) { + /* + * Direct EC access mode: sensors at registers 0x78-0x7F, + * 0xC0-0xC7. Registers return 0x00 for non-implemented, + * thermal sensors return 0x80 when not available. + * + * In some special cases (when Power Supply ID is 0xC2) + * above rule causes thermal control issues. Offset 0xEF + * determines EC version. 0xC0-0xC7 are not thermal registers + * in Ver 3. + */ + if (!acpi_ec_read(TP_EC_FUNCREV, &ver)) + pr_warn("Thinkpad ACPI EC unable to access EC version\n"); + + /* Quirks to check non-standard EC */ + thermal_with_ns_address = tpacpi_check_quirks(thermal_quirk_table, + ARRAY_SIZE(thermal_quirk_table)); + + /* Support for Thinkpads with non-standard address */ + if (thermal_with_ns_address) { + pr_info("ECFW with non-standard thermal registers found\n"); + return TPACPI_THERMAL_TPEC_12; + } + + ta1 = ta2 = 0; + for (i = 0; i < 8; i++) { + if (acpi_ec_read(TP_EC_THERMAL_TMP0 + i, &t)) { + ta1 |= t; + } else { + ta1 = 0; + break; + } + if (ver < 3) { + if (acpi_ec_read(TP_EC_THERMAL_TMP8 + i, &t)) { + ta2 |= t; + } else { + ta1 = 0; + break; + } + } + } + + if (ta1 == 0) { + /* This is sheer paranoia, but we handle it anyway */ + if (acpi_tmp7) { + pr_err("ThinkPad ACPI EC access misbehaving, falling back to ACPI TMPx access mode\n"); + return TPACPI_THERMAL_ACPI_TMP07; + } + pr_err("ThinkPad ACPI EC access misbehaving, disabling thermal sensors access\n"); + return TPACPI_THERMAL_NONE; + } + + if (ver >= 3) { + thermal_use_labels = true; + return TPACPI_THERMAL_TPEC_8; + } + + return (ta2 != 0) ? TPACPI_THERMAL_TPEC_16 : TPACPI_THERMAL_TPEC_8; + } + + if (acpi_tmp7) { + if (tpacpi_is_ibm() && acpi_evalf(ec_handle, NULL, "UPDT", "qv")) { + /* 600e/x, 770e, 770x */ + return TPACPI_THERMAL_ACPI_UPDT; + } + /* IBM/LENOVO DSDT EC.TMPx access, max 8 sensors */ + return TPACPI_THERMAL_ACPI_TMP07; + } + + /* temperatures not supported on 570, G4x, R30, R31, R32 */ + return TPACPI_THERMAL_NONE; +} + +/* idx is zero-based */ +static int thermal_get_sensor(int idx, s32 *value) +{ + int t; + s8 tmp; + char tmpi[5]; + + t = TP_EC_THERMAL_TMP0; + + switch (thermal_read_mode) { +#if TPACPI_MAX_THERMAL_SENSORS >= 16 + case TPACPI_THERMAL_TPEC_16: + if (idx >= 8 && idx <= 15) { + t = TP_EC_THERMAL_TMP8; + idx -= 8; + } +#endif + fallthrough; + case TPACPI_THERMAL_TPEC_8: + if (idx <= 7) { + if (!acpi_ec_read(t + idx, &tmp)) + return -EIO; + *value = tmp * 1000; + return 0; + } + break; + + /* The Non-standard EC uses 12 Thermal areas */ + case TPACPI_THERMAL_TPEC_12: + if (idx >= 12) + return -EINVAL; + + t = idx < 8 ? TP_EC_THERMAL_TMP0_NS + idx : + TP_EC_THERMAL_TMP8_NS + (idx - 8); + + if (!acpi_ec_read(t, &tmp)) + return -EIO; + + *value = tmp * MILLIDEGREE_PER_DEGREE; + return 0; + + case TPACPI_THERMAL_ACPI_UPDT: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, NULL, "UPDT", "v")) + return -EIO; + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + *value = (t - 2732) * 100; + return 0; + } + break; + + case TPACPI_THERMAL_ACPI_TMP07: + if (idx <= 7) { + snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); + if (!acpi_evalf(ec_handle, &t, tmpi, "d")) + return -EIO; + if (t > 127 || t < -127) + t = TP_EC_THERMAL_TMP_NA; + *value = t * 1000; + return 0; + } + break; + + case TPACPI_THERMAL_NONE: + default: + return -ENOSYS; + } + + return -EINVAL; +} + +static int thermal_get_sensors(struct ibm_thermal_sensors_struct *s) +{ + int res, i, n; + + if (!s) + return -EINVAL; + + if (thermal_read_mode == TPACPI_THERMAL_TPEC_16) + n = 16; + else if (thermal_read_mode == TPACPI_THERMAL_TPEC_12) + n = 12; + else + n = 8; + + for (i = 0 ; i < n; i++) { + res = thermal_get_sensor(i, &s->temp[i]); + if (res) + return res; + } + + return n; +} + +static void thermal_dump_all_sensors(void) +{ + int n, i; + struct ibm_thermal_sensors_struct t; + + n = thermal_get_sensors(&t); + if (n <= 0) + return; + + pr_notice("temperatures (Celsius):"); + + for (i = 0; i < n; i++) { + if (t.temp[i] != TPACPI_THERMAL_SENSOR_NA) + pr_cont(" %d", (int)(t.temp[i] / 1000)); + else + pr_cont(" N/A"); + } + + pr_cont("\n"); +} + +/* sysfs temp##_input -------------------------------------------------- */ + +static ssize_t thermal_temp_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute *sensor_attr = + to_sensor_dev_attr(attr); + int idx = sensor_attr->index; + s32 value; + int res; + + res = thermal_get_sensor(idx, &value); + if (res) + return res; + if (value == TPACPI_THERMAL_SENSOR_NA) + return -ENXIO; + + return sysfs_emit(buf, "%d\n", value); +} + +#define THERMAL_SENSOR_ATTR_TEMP(_idxA, _idxB) \ + SENSOR_ATTR(temp##_idxA##_input, S_IRUGO, \ + thermal_temp_input_show, NULL, _idxB) + +static struct sensor_device_attribute sensor_dev_attr_thermal_temp_input[] = { + THERMAL_SENSOR_ATTR_TEMP(1, 0), + THERMAL_SENSOR_ATTR_TEMP(2, 1), + THERMAL_SENSOR_ATTR_TEMP(3, 2), + THERMAL_SENSOR_ATTR_TEMP(4, 3), + THERMAL_SENSOR_ATTR_TEMP(5, 4), + THERMAL_SENSOR_ATTR_TEMP(6, 5), + THERMAL_SENSOR_ATTR_TEMP(7, 6), + THERMAL_SENSOR_ATTR_TEMP(8, 7), + THERMAL_SENSOR_ATTR_TEMP(9, 8), + THERMAL_SENSOR_ATTR_TEMP(10, 9), + THERMAL_SENSOR_ATTR_TEMP(11, 10), + THERMAL_SENSOR_ATTR_TEMP(12, 11), + THERMAL_SENSOR_ATTR_TEMP(13, 12), + THERMAL_SENSOR_ATTR_TEMP(14, 13), + THERMAL_SENSOR_ATTR_TEMP(15, 14), + THERMAL_SENSOR_ATTR_TEMP(16, 15), +}; + +#define THERMAL_ATTRS(X) \ + &sensor_dev_attr_thermal_temp_input[X].dev_attr.attr + +static struct attribute *thermal_temp_input_attr[] = { + THERMAL_ATTRS(0), + THERMAL_ATTRS(1), + THERMAL_ATTRS(2), + THERMAL_ATTRS(3), + THERMAL_ATTRS(4), + THERMAL_ATTRS(5), + THERMAL_ATTRS(6), + THERMAL_ATTRS(7), + THERMAL_ATTRS(8), + THERMAL_ATTRS(9), + THERMAL_ATTRS(10), + THERMAL_ATTRS(11), + THERMAL_ATTRS(12), + THERMAL_ATTRS(13), + THERMAL_ATTRS(14), + THERMAL_ATTRS(15), + NULL +}; + +#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr) + +static umode_t thermal_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device_attribute *dev_attr = to_dev_attr(attr); + struct sensor_device_attribute *sensor_attr = + to_sensor_dev_attr(dev_attr); + + int idx = sensor_attr->index; + + switch (thermal_read_mode) { + case TPACPI_THERMAL_NONE: + return 0; + + case TPACPI_THERMAL_ACPI_TMP07: + case TPACPI_THERMAL_ACPI_UPDT: + case TPACPI_THERMAL_TPEC_8: + if (idx >= 8) + return 0; + break; + + case TPACPI_THERMAL_TPEC_12: + if (idx >= 12) + return 0; + break; + + default: + break; + + } + + return attr->mode; +} + +static const struct attribute_group thermal_attr_group = { + .is_visible = thermal_attr_is_visible, + .attrs = thermal_temp_input_attr, +}; + +#undef THERMAL_SENSOR_ATTR_TEMP +#undef THERMAL_ATTRS + +static ssize_t temp1_label_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "CPU\n"); +} +static DEVICE_ATTR_RO(temp1_label); + +static ssize_t temp2_label_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "GPU\n"); +} +static DEVICE_ATTR_RO(temp2_label); + +static struct attribute *temp_label_attributes[] = { + &dev_attr_temp1_label.attr, + &dev_attr_temp2_label.attr, + NULL +}; + +static umode_t temp_label_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return thermal_use_labels ? attr->mode : 0; +} + +static const struct attribute_group temp_label_attr_group = { + .is_visible = temp_label_attr_is_visible, + .attrs = temp_label_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static int __init thermal_init(struct ibm_init_struct *iibm) +{ + vdbg_printk(TPACPI_DBG_INIT, "initializing thermal subdriver\n"); + + thermal_read_mode = thermal_read_mode_check(); + + vdbg_printk(TPACPI_DBG_INIT, "thermal is %s, mode %d\n", + str_supported(thermal_read_mode != TPACPI_THERMAL_NONE), + thermal_read_mode); + + return thermal_read_mode != TPACPI_THERMAL_NONE ? 0 : -ENODEV; +} + +static int thermal_read(struct seq_file *m) +{ + int n, i; + struct ibm_thermal_sensors_struct t; + + n = thermal_get_sensors(&t); + if (unlikely(n < 0)) + return n; + + seq_printf(m, "temperatures:\t"); + + if (n > 0) { + for (i = 0; i < (n - 1); i++) + seq_printf(m, "%d ", t.temp[i] / 1000); + seq_printf(m, "%d\n", t.temp[i] / 1000); + } else + seq_printf(m, "not supported\n"); + + return 0; +} + +static struct ibm_struct thermal_driver_data = { + .name = "thermal", + .read = thermal_read, +}; + +/************************************************************************* + * Backlight/brightness subdriver + */ + +#define TPACPI_BACKLIGHT_DEV_NAME "thinkpad_screen" + +/* + * ThinkPads can read brightness from two places: EC HBRV (0x31), or + * CMOS NVRAM byte 0x5E, bits 0-3. + * + * EC HBRV (0x31) has the following layout + * Bit 7: unknown function + * Bit 6: unknown function + * Bit 5: Z: honour scale changes, NZ: ignore scale changes + * Bit 4: must be set to zero to avoid problems + * Bit 3-0: backlight brightness level + * + * brightness_get_raw returns status data in the HBRV layout + * + * WARNING: The X61 has been verified to use HBRV for something else, so + * this should be used _only_ on IBM ThinkPads, and maybe with some careful + * testing on the very early *60 Lenovo models... + */ + +enum { + TP_EC_BACKLIGHT = 0x31, + + /* TP_EC_BACKLIGHT bitmasks */ + TP_EC_BACKLIGHT_LVLMSK = 0x1F, + TP_EC_BACKLIGHT_CMDMSK = 0xE0, + TP_EC_BACKLIGHT_MAPSW = 0x20, +}; + +enum tpacpi_brightness_access_mode { + TPACPI_BRGHT_MODE_AUTO = 0, /* Not implemented yet */ + TPACPI_BRGHT_MODE_EC, /* EC control */ + TPACPI_BRGHT_MODE_UCMS_STEP, /* UCMS step-based control */ + TPACPI_BRGHT_MODE_ECNVRAM, /* EC control w/ NVRAM store */ + TPACPI_BRGHT_MODE_MAX +}; + +static struct backlight_device *ibm_backlight_device; + +static enum tpacpi_brightness_access_mode brightness_mode = + TPACPI_BRGHT_MODE_MAX; + +static unsigned int brightness_enable = 2; /* 2 = auto, 0 = no, 1 = yes */ + +static struct mutex brightness_mutex; + +/* NVRAM brightness access */ +static unsigned int tpacpi_brightness_nvram_get(void) +{ + u8 lnvram; + + lockdep_assert_held(&brightness_mutex); + + lnvram = (nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS) + & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; + lnvram &= bright_maxlvl; + + return lnvram; +} + +static void tpacpi_brightness_checkpoint_nvram(void) +{ + u8 lec = 0; + u8 b_nvram; + + if (brightness_mode != TPACPI_BRGHT_MODE_ECNVRAM) + return; + + vdbg_printk(TPACPI_DBG_BRGHT, + "trying to checkpoint backlight level to NVRAM...\n"); + + if (mutex_lock_killable(&brightness_mutex) < 0) + return; + + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + goto unlock; + lec &= TP_EC_BACKLIGHT_LVLMSK; + b_nvram = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); + + if (lec != ((b_nvram & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) + >> TP_NVRAM_POS_LEVEL_BRIGHTNESS)) { + /* NVRAM needs update */ + b_nvram &= ~(TP_NVRAM_MASK_LEVEL_BRIGHTNESS << + TP_NVRAM_POS_LEVEL_BRIGHTNESS); + b_nvram |= lec; + nvram_write_byte(b_nvram, TP_NVRAM_ADDR_BRIGHTNESS); + dbg_printk(TPACPI_DBG_BRGHT, + "updated NVRAM backlight level to %u (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } else + vdbg_printk(TPACPI_DBG_BRGHT, + "NVRAM backlight level already is %u (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + +unlock: + mutex_unlock(&brightness_mutex); +} + + +static int tpacpi_brightness_get_raw(int *status) +{ + u8 lec = 0; + + lockdep_assert_held(&brightness_mutex); + + switch (brightness_mode) { + case TPACPI_BRGHT_MODE_UCMS_STEP: + *status = tpacpi_brightness_nvram_get(); + return 0; + case TPACPI_BRGHT_MODE_EC: + case TPACPI_BRGHT_MODE_ECNVRAM: + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + return -EIO; + *status = lec; + return 0; + default: + return -ENXIO; + } +} + +/* do NOT call with illegal backlight level value */ +static int tpacpi_brightness_set_ec(unsigned int value) +{ + u8 lec = 0; + + lockdep_assert_held(&brightness_mutex); + + if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) + return -EIO; + + if (unlikely(!acpi_ec_write(TP_EC_BACKLIGHT, + (lec & TP_EC_BACKLIGHT_CMDMSK) | + (value & TP_EC_BACKLIGHT_LVLMSK)))) + return -EIO; + + return 0; +} + +static int tpacpi_brightness_set_ucmsstep(unsigned int value) +{ + int cmos_cmd, inc; + unsigned int current_value, i; + + lockdep_assert_held(&brightness_mutex); + + current_value = tpacpi_brightness_nvram_get(); + + if (value == current_value) + return 0; + + cmos_cmd = (value > current_value) ? + TP_CMOS_BRIGHTNESS_UP : + TP_CMOS_BRIGHTNESS_DOWN; + inc = (value > current_value) ? 1 : -1; + + for (i = current_value; i != value; i += inc) + if (issue_thinkpad_cmos_command(cmos_cmd)) + return -EIO; + + return 0; +} + +/* May return EINTR which can always be mapped to ERESTARTSYS */ +static int brightness_set(unsigned int value) +{ + int res; + + if (value > bright_maxlvl) + return -EINVAL; + + vdbg_printk(TPACPI_DBG_BRGHT, + "set backlight level to %d\n", value); + + res = mutex_lock_killable(&brightness_mutex); + if (res < 0) + return res; + + switch (brightness_mode) { + case TPACPI_BRGHT_MODE_EC: + case TPACPI_BRGHT_MODE_ECNVRAM: + res = tpacpi_brightness_set_ec(value); + break; + case TPACPI_BRGHT_MODE_UCMS_STEP: + res = tpacpi_brightness_set_ucmsstep(value); + break; + default: + res = -ENXIO; + } + + mutex_unlock(&brightness_mutex); + return res; +} + +/* sysfs backlight class ----------------------------------------------- */ + +static int brightness_update_status(struct backlight_device *bd) +{ + int level = backlight_get_brightness(bd); + + dbg_printk(TPACPI_DBG_BRGHT, + "backlight: attempt to set level to %d\n", + level); + + /* it is the backlight class's job (caller) to handle + * EINTR and other errors properly */ + return brightness_set(level); +} + +static int brightness_get(struct backlight_device *bd) +{ + int status, res; + + res = mutex_lock_killable(&brightness_mutex); + if (res < 0) + return 0; + + res = tpacpi_brightness_get_raw(&status); + + mutex_unlock(&brightness_mutex); + + if (res < 0) + return 0; + + return status & TP_EC_BACKLIGHT_LVLMSK; +} + +static void tpacpi_brightness_notify_change(void) +{ + backlight_force_update(ibm_backlight_device, + BACKLIGHT_UPDATE_HOTKEY); +} + +static const struct backlight_ops ibm_backlight_data = { + .get_brightness = brightness_get, + .update_status = brightness_update_status, +}; + +/* --------------------------------------------------------------------- */ + +static int __init tpacpi_evaluate_bcl(struct acpi_device *adev, void *not_used) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + int rc; + + status = acpi_evaluate_object(adev->handle, "_BCL", NULL, &buffer); + if (ACPI_FAILURE(status)) + return 0; + + obj = buffer.pointer; + if (!obj || obj->type != ACPI_TYPE_PACKAGE) { + acpi_handle_info(adev->handle, + "Unknown _BCL data, please report this to %s\n", + TPACPI_MAIL); + rc = 0; + } else { + rc = obj->package.count; + } + kfree(obj); + + return rc; +} + +/* + * Call _BCL method of video device. On some ThinkPads this will + * switch the firmware to the ACPI brightness control mode. + */ + +static int __init tpacpi_query_bcl_levels(acpi_handle handle) +{ + struct acpi_device *device; + + device = acpi_fetch_acpi_dev(handle); + if (!device) + return 0; + + return acpi_dev_for_each_child(device, tpacpi_evaluate_bcl, NULL); +} + + +/* + * Returns 0 (no ACPI _BCL or _BCL invalid), or size of brightness map + */ +static unsigned int __init tpacpi_check_std_acpi_brightness_support(void) +{ + acpi_handle video_device; + int bcl_levels = 0; + + tpacpi_acpi_handle_locate("video", NULL, &video_device); + if (video_device) + bcl_levels = tpacpi_query_bcl_levels(video_device); + + tp_features.bright_acpimode = (bcl_levels > 0); + + return (bcl_levels > 2) ? (bcl_levels - 2) : 0; +} + +/* + * These are only useful for models that have only one possibility + * of GPU. If the BIOS model handles both ATI and Intel, don't use + * these quirks. + */ +#define TPACPI_BRGHT_Q_NOEC 0x0001 /* Must NOT use EC HBRV */ +#define TPACPI_BRGHT_Q_EC 0x0002 /* Should or must use EC HBRV */ +#define TPACPI_BRGHT_Q_ASK 0x8000 /* Ask for user report */ + +static const struct tpacpi_quirk brightness_quirk_table[] __initconst = { + /* Models with ATI GPUs known to require ECNVRAM mode */ + TPACPI_Q_IBM('1', 'Y', TPACPI_BRGHT_Q_EC), /* T43/p ATI */ + + /* Models with ATI GPUs that can use ECNVRAM */ + TPACPI_Q_IBM('1', 'R', TPACPI_BRGHT_Q_EC), /* R50,51 T40-42 */ + TPACPI_Q_IBM('1', 'Q', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + TPACPI_Q_IBM('7', '6', TPACPI_BRGHT_Q_EC), /* R52 */ + TPACPI_Q_IBM('7', '8', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + + /* Models with Intel Extreme Graphics 2 */ + TPACPI_Q_IBM('1', 'U', TPACPI_BRGHT_Q_NOEC), /* X40 */ + TPACPI_Q_IBM('1', 'V', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + TPACPI_Q_IBM('1', 'W', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), + + /* Models with Intel GMA900 */ + TPACPI_Q_IBM('7', '0', TPACPI_BRGHT_Q_NOEC), /* T43, R52 */ + TPACPI_Q_IBM('7', '4', TPACPI_BRGHT_Q_NOEC), /* X41 */ + TPACPI_Q_IBM('7', '5', TPACPI_BRGHT_Q_NOEC), /* X41 Tablet */ +}; + +/* + * Returns < 0 for error, otherwise sets tp_features.bright_* + * and bright_maxlvl. + */ +static void __init tpacpi_detect_brightness_capabilities(void) +{ + unsigned int b; + + vdbg_printk(TPACPI_DBG_INIT, + "detecting firmware brightness interface capabilities\n"); + + /* we could run a quirks check here (same table used by + * brightness_init) if needed */ + + /* + * We always attempt to detect acpi support, so as to switch + * Lenovo Vista BIOS to ACPI brightness mode even if we are not + * going to publish a backlight interface + */ + b = tpacpi_check_std_acpi_brightness_support(); + switch (b) { + case 16: + bright_maxlvl = 15; + break; + case 8: + case 0: + bright_maxlvl = 7; + break; + default: + tp_features.bright_unkfw = 1; + bright_maxlvl = b - 1; + } + pr_debug("detected %u brightness levels\n", bright_maxlvl + 1); +} + +static int __init brightness_init(struct ibm_init_struct *iibm) +{ + struct backlight_properties props; + int b; + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT, "initializing brightness subdriver\n"); + + mutex_init(&brightness_mutex); + + quirks = tpacpi_check_quirks(brightness_quirk_table, + ARRAY_SIZE(brightness_quirk_table)); + + /* tpacpi_detect_brightness_capabilities() must have run already */ + + /* if it is unknown, we don't handle it: it wouldn't be safe */ + if (tp_features.bright_unkfw) + return -ENODEV; + + if (!brightness_enable) { + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness support disabled by module parameter\n"); + return -ENODEV; + } + + if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { + if (brightness_enable > 1) { + pr_info("Standard ACPI backlight interface available, not loading native one\n"); + return -ENODEV; + } else if (brightness_enable == 1) { + pr_warn("Cannot enable backlight brightness support, ACPI is already handling it. Refer to the acpi_backlight kernel parameter.\n"); + return -ENODEV; + } + } else if (!tp_features.bright_acpimode) { + pr_notice("ACPI backlight interface not available\n"); + return -ENODEV; + } + + pr_notice("ACPI native brightness control enabled\n"); + + /* + * Check for module parameter bogosity, note that we + * init brightness_mode to TPACPI_BRGHT_MODE_MAX in order to be + * able to detect "unspecified" + */ + if (brightness_mode > TPACPI_BRGHT_MODE_MAX) + return -EINVAL; + + /* TPACPI_BRGHT_MODE_AUTO not implemented yet, just use default */ + if (brightness_mode == TPACPI_BRGHT_MODE_AUTO || + brightness_mode == TPACPI_BRGHT_MODE_MAX) { + if (quirks & TPACPI_BRGHT_Q_EC) + brightness_mode = TPACPI_BRGHT_MODE_ECNVRAM; + else + brightness_mode = TPACPI_BRGHT_MODE_UCMS_STEP; + + dbg_printk(TPACPI_DBG_BRGHT, + "driver auto-selected brightness_mode=%d\n", + brightness_mode); + } + + /* Safety */ + if (!tpacpi_is_ibm() && + (brightness_mode == TPACPI_BRGHT_MODE_ECNVRAM || + brightness_mode == TPACPI_BRGHT_MODE_EC)) + return -EINVAL; + + if (tpacpi_brightness_get_raw(&b) < 0) + return -ENODEV; + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_PLATFORM; + props.max_brightness = bright_maxlvl; + props.brightness = b & TP_EC_BACKLIGHT_LVLMSK; + ibm_backlight_device = backlight_device_register(TPACPI_BACKLIGHT_DEV_NAME, + NULL, NULL, + &ibm_backlight_data, + &props); + if (IS_ERR(ibm_backlight_device)) { + int rc = PTR_ERR(ibm_backlight_device); + ibm_backlight_device = NULL; + pr_err("Could not register backlight device\n"); + return rc; + } + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness is supported\n"); + + if (quirks & TPACPI_BRGHT_Q_ASK) { + pr_notice("brightness: will use unverified default: brightness_mode=%d\n", + brightness_mode); + pr_notice("brightness: please report to %s whether it works well or not on your ThinkPad\n", + TPACPI_MAIL); + } + + /* Added by mistake in early 2007. Probably useless, but it could + * be working around some unknown firmware problem where the value + * read at startup doesn't match the real hardware state... so leave + * it in place just in case */ + backlight_update_status(ibm_backlight_device); + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, + "brightness: registering brightness hotkeys as change notification\n"); + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask + | TP_ACPI_HKEY_BRGHTUP_MASK + | TP_ACPI_HKEY_BRGHTDWN_MASK); + return 0; +} + +static void brightness_suspend(void) +{ + tpacpi_brightness_checkpoint_nvram(); +} + +static void brightness_shutdown(void) +{ + tpacpi_brightness_checkpoint_nvram(); +} + +static void brightness_exit(void) +{ + if (ibm_backlight_device) { + vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_BRGHT, + "calling backlight_device_unregister()\n"); + backlight_device_unregister(ibm_backlight_device); + } + + tpacpi_brightness_checkpoint_nvram(); +} + +static int brightness_read(struct seq_file *m) +{ + int level; + + level = brightness_get(NULL); + if (level < 0) { + seq_printf(m, "level:\t\tunreadable\n"); + } else { + seq_printf(m, "level:\t\t%d\n", level); + seq_printf(m, "commands:\tup, down\n"); + seq_printf(m, "commands:\tlevel ( is 0-%d)\n", + bright_maxlvl); + } + + return 0; +} + +static int brightness_write(char *buf) +{ + int level; + int rc; + char *cmd; + + level = brightness_get(NULL); + if (level < 0) + return level; + + while ((cmd = strsep(&buf, ","))) { + if (strstarts(cmd, "up")) { + if (level < bright_maxlvl) + level++; + } else if (strstarts(cmd, "down")) { + if (level > 0) + level--; + } else if (sscanf(cmd, "level %d", &level) == 1 && + level >= 0 && level <= bright_maxlvl) { + /* new level set */ + } else + return -EINVAL; + } + + tpacpi_disclose_usertask("procfs brightness", + "set level to %d\n", level); + + /* + * Now we know what the final level should be, so we try to set it. + * Doing it this way makes the syscall restartable in case of EINTR + */ + rc = brightness_set(level); + if (!rc && ibm_backlight_device) + backlight_force_update(ibm_backlight_device, + BACKLIGHT_UPDATE_SYSFS); + return (rc == -EINTR) ? -ERESTARTSYS : rc; +} + +static struct ibm_struct brightness_driver_data = { + .name = "brightness", + .read = brightness_read, + .write = brightness_write, + .exit = brightness_exit, + .suspend = brightness_suspend, + .shutdown = brightness_shutdown, +}; + +/************************************************************************* + * Volume subdriver + */ + +/* + * IBM ThinkPads have a simple volume controller with MUTE gating. + * Very early Lenovo ThinkPads follow the IBM ThinkPad spec. + * + * Since the *61 series (and probably also the later *60 series), Lenovo + * ThinkPads only implement the MUTE gate. + * + * EC register 0x30 + * Bit 6: MUTE (1 mutes sound) + * Bit 3-0: Volume + * Other bits should be zero as far as we know. + * + * This is also stored in CMOS NVRAM, byte 0x60, bit 6 (MUTE), and + * bits 3-0 (volume). Other bits in NVRAM may have other functions, + * such as bit 7 which is used to detect repeated presses of MUTE, + * and we leave them unchanged. + * + * On newer Lenovo ThinkPads, the EC can automatically change the volume + * in response to user input. Unfortunately, this rarely works well. + * The laptop changes the state of its internal MUTE gate and, on some + * models, sends KEY_MUTE, causing any user code that responds to the + * mute button to get confused. The hardware MUTE gate is also + * unnecessary, since user code can handle the mute button without + * kernel or EC help. + * + * To avoid confusing userspace, we simply disable all EC-based mute + * and volume controls when possible. + */ + +#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT + +#define TPACPI_ALSA_DRVNAME "ThinkPad EC" +#define TPACPI_ALSA_SHRTNAME "ThinkPad Console Audio Control" +#define TPACPI_ALSA_MIXERNAME TPACPI_ALSA_SHRTNAME + +#if SNDRV_CARDS <= 32 +#define DEFAULT_ALSA_IDX ~((1 << (SNDRV_CARDS - 3)) - 1) +#else +#define DEFAULT_ALSA_IDX ~((1 << (32 - 3)) - 1) +#endif +static int alsa_index = DEFAULT_ALSA_IDX; /* last three slots */ +static char *alsa_id = "ThinkPadEC"; +static bool alsa_enable = SNDRV_DEFAULT_ENABLE1; + +struct tpacpi_alsa_data { + struct snd_card *card; + struct snd_ctl_elem_id *ctl_mute_id; + struct snd_ctl_elem_id *ctl_vol_id; +}; + +static struct snd_card *alsa_card; + +enum { + TP_EC_AUDIO = 0x30, + + /* TP_EC_AUDIO bits */ + TP_EC_AUDIO_MUTESW = 6, + + /* TP_EC_AUDIO bitmasks */ + TP_EC_AUDIO_LVL_MSK = 0x0F, + TP_EC_AUDIO_MUTESW_MSK = (1 << TP_EC_AUDIO_MUTESW), + + /* Maximum volume */ + TP_EC_VOLUME_MAX = 14, +}; + +enum tpacpi_volume_access_mode { + TPACPI_VOL_MODE_AUTO = 0, /* Not implemented yet */ + TPACPI_VOL_MODE_EC, /* Pure EC control */ + TPACPI_VOL_MODE_UCMS_STEP, /* UCMS step-based control: N/A */ + TPACPI_VOL_MODE_ECNVRAM, /* EC control w/ NVRAM store */ + TPACPI_VOL_MODE_MAX +}; + +enum tpacpi_volume_capabilities { + TPACPI_VOL_CAP_AUTO = 0, /* Use white/blacklist */ + TPACPI_VOL_CAP_VOLMUTE, /* Output vol and mute */ + TPACPI_VOL_CAP_MUTEONLY, /* Output mute only */ + TPACPI_VOL_CAP_MAX +}; + +enum tpacpi_mute_btn_mode { + TP_EC_MUTE_BTN_LATCH = 0, /* Mute mutes; up/down unmutes */ + /* We don't know what mode 1 is. */ + TP_EC_MUTE_BTN_NONE = 2, /* Mute and up/down are just keys */ + TP_EC_MUTE_BTN_TOGGLE = 3, /* Mute toggles; up/down unmutes */ +}; + +static enum tpacpi_volume_access_mode volume_mode = + TPACPI_VOL_MODE_MAX; + +static enum tpacpi_volume_capabilities volume_capabilities; +static bool volume_control_allowed; +static bool software_mute_requested = true; +static bool software_mute_active; +static int software_mute_orig_mode; + +/* + * Used to syncronize writers to TP_EC_AUDIO and + * TP_NVRAM_ADDR_MIXER, as we need to do read-modify-write + */ +static struct mutex volume_mutex; + +static void tpacpi_volume_checkpoint_nvram(void) +{ + u8 lec = 0; + u8 b_nvram; + u8 ec_mask; + + if (volume_mode != TPACPI_VOL_MODE_ECNVRAM) + return; + if (!volume_control_allowed) + return; + if (software_mute_active) + return; + + vdbg_printk(TPACPI_DBG_MIXER, + "trying to checkpoint mixer state to NVRAM...\n"); + + if (tp_features.mixer_no_level_control) + ec_mask = TP_EC_AUDIO_MUTESW_MSK; + else + ec_mask = TP_EC_AUDIO_MUTESW_MSK | TP_EC_AUDIO_LVL_MSK; + + if (mutex_lock_killable(&volume_mutex) < 0) + return; + + if (unlikely(!acpi_ec_read(TP_EC_AUDIO, &lec))) + goto unlock; + lec &= ec_mask; + b_nvram = nvram_read_byte(TP_NVRAM_ADDR_MIXER); + + if (lec != (b_nvram & ec_mask)) { + /* NVRAM needs update */ + b_nvram &= ~ec_mask; + b_nvram |= lec; + nvram_write_byte(b_nvram, TP_NVRAM_ADDR_MIXER); + dbg_printk(TPACPI_DBG_MIXER, + "updated NVRAM mixer status to 0x%02x (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } else { + vdbg_printk(TPACPI_DBG_MIXER, + "NVRAM mixer status already is 0x%02x (0x%02x)\n", + (unsigned int) lec, (unsigned int) b_nvram); + } + +unlock: + mutex_unlock(&volume_mutex); +} + +static int volume_get_status_ec(u8 *status) +{ + u8 s; + + if (!acpi_ec_read(TP_EC_AUDIO, &s)) + return -EIO; + + *status = s; + + dbg_printk(TPACPI_DBG_MIXER, "status 0x%02x\n", s); + + return 0; +} + +static int volume_get_status(u8 *status) +{ + return volume_get_status_ec(status); +} + +static int volume_set_status_ec(const u8 status) +{ + if (!acpi_ec_write(TP_EC_AUDIO, status)) + return -EIO; + + dbg_printk(TPACPI_DBG_MIXER, "set EC mixer to 0x%02x\n", status); + + /* + * On X200s, and possibly on others, it can take a while for + * reads to become correct. + */ + msleep(1); + + return 0; +} + +static int volume_set_status(const u8 status) +{ + return volume_set_status_ec(status); +} + +/* returns < 0 on error, 0 on no change, 1 on change */ +static int __volume_set_mute_ec(const bool mute) +{ + int rc; + u8 s, n; + + if (mutex_lock_killable(&volume_mutex) < 0) + return -EINTR; + + rc = volume_get_status_ec(&s); + if (rc) + goto unlock; + + n = (mute) ? s | TP_EC_AUDIO_MUTESW_MSK : + s & ~TP_EC_AUDIO_MUTESW_MSK; + + if (n != s) { + rc = volume_set_status_ec(n); + if (!rc) + rc = 1; + } + +unlock: + mutex_unlock(&volume_mutex); + return rc; +} + +static int volume_alsa_set_mute(const bool mute) +{ + dbg_printk(TPACPI_DBG_MIXER, "ALSA: trying to %smute\n", + (mute) ? "" : "un"); + return __volume_set_mute_ec(mute); +} + +static int volume_set_mute(const bool mute) +{ + int rc; + + dbg_printk(TPACPI_DBG_MIXER, "trying to %smute\n", + (mute) ? "" : "un"); + + rc = __volume_set_mute_ec(mute); + return (rc < 0) ? rc : 0; +} + +/* returns < 0 on error, 0 on no change, 1 on change */ +static int __volume_set_volume_ec(const u8 vol) +{ + int rc; + u8 s, n; + + if (vol > TP_EC_VOLUME_MAX) + return -EINVAL; + + if (mutex_lock_killable(&volume_mutex) < 0) + return -EINTR; + + rc = volume_get_status_ec(&s); + if (rc) + goto unlock; + + n = (s & ~TP_EC_AUDIO_LVL_MSK) | vol; + + if (n != s) { + rc = volume_set_status_ec(n); + if (!rc) + rc = 1; + } + +unlock: + mutex_unlock(&volume_mutex); + return rc; +} + +static int volume_set_software_mute(bool startup) +{ + int result; + + if (!tpacpi_is_lenovo()) + return -ENODEV; + + if (startup) { + if (!acpi_evalf(ec_handle, &software_mute_orig_mode, + "HAUM", "qd")) + return -EIO; + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "Initial HAUM setting was %d\n", + software_mute_orig_mode); + } + + if (!acpi_evalf(ec_handle, &result, "SAUM", "qdd", + (int)TP_EC_MUTE_BTN_NONE)) + return -EIO; + + if (result != TP_EC_MUTE_BTN_NONE) + pr_warn("Unexpected SAUM result %d\n", + result); + + /* + * In software mute mode, the standard codec controls take + * precendence, so we unmute the ThinkPad HW switch at + * startup. Just on case there are SAUM-capable ThinkPads + * with level controls, set max HW volume as well. + */ + if (tp_features.mixer_no_level_control) + result = volume_set_mute(false); + else + result = volume_set_status(TP_EC_VOLUME_MAX); + + if (result != 0) + pr_warn("Failed to unmute the HW mute switch\n"); + + return 0; +} + +static void volume_exit_software_mute(void) +{ + int r; + + if (!acpi_evalf(ec_handle, &r, "SAUM", "qdd", software_mute_orig_mode) + || r != software_mute_orig_mode) + pr_warn("Failed to restore mute mode\n"); +} + +static int volume_alsa_set_volume(const u8 vol) +{ + dbg_printk(TPACPI_DBG_MIXER, + "ALSA: trying to set volume level to %hu\n", vol); + return __volume_set_volume_ec(vol); +} + +static void volume_alsa_notify_change(void) +{ + struct tpacpi_alsa_data *d; + + if (alsa_card && alsa_card->private_data) { + d = alsa_card->private_data; + if (d->ctl_mute_id) + snd_ctl_notify(alsa_card, + SNDRV_CTL_EVENT_MASK_VALUE, + d->ctl_mute_id); + if (d->ctl_vol_id) + snd_ctl_notify(alsa_card, + SNDRV_CTL_EVENT_MASK_VALUE, + d->ctl_vol_id); + } +} + +static int volume_alsa_vol_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = TP_EC_VOLUME_MAX; + return 0; +} + +static int volume_alsa_vol_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + u8 s; + int rc; + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + ucontrol->value.integer.value[0] = s & TP_EC_AUDIO_LVL_MSK; + return 0; +} + +static int volume_alsa_vol_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + tpacpi_disclose_usertask("ALSA", "set volume to %ld\n", + ucontrol->value.integer.value[0]); + return volume_alsa_set_volume(ucontrol->value.integer.value[0]); +} + +#define volume_alsa_mute_info snd_ctl_boolean_mono_info + +static int volume_alsa_mute_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + u8 s; + int rc; + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + ucontrol->value.integer.value[0] = + (s & TP_EC_AUDIO_MUTESW_MSK) ? 0 : 1; + return 0; +} + +static int volume_alsa_mute_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + tpacpi_disclose_usertask("ALSA", "%smute\n", + ucontrol->value.integer.value[0] ? + "un" : ""); + return volume_alsa_set_mute(!ucontrol->value.integer.value[0]); +} + +static struct snd_kcontrol_new volume_alsa_control_vol __initdata = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Console Playback Volume", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READ, + .info = volume_alsa_vol_info, + .get = volume_alsa_vol_get, +}; + +static struct snd_kcontrol_new volume_alsa_control_mute __initdata = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Console Playback Switch", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READ, + .info = volume_alsa_mute_info, + .get = volume_alsa_mute_get, +}; + +static void volume_suspend(void) +{ + tpacpi_volume_checkpoint_nvram(); +} + +static void volume_resume(void) +{ + if (software_mute_active) { + if (volume_set_software_mute(false) < 0) + pr_warn("Failed to restore software mute\n"); + } else { + volume_alsa_notify_change(); + } +} + +static void volume_shutdown(void) +{ + tpacpi_volume_checkpoint_nvram(); +} + +static void volume_exit(void) +{ + if (alsa_card) { + snd_card_free(alsa_card); + alsa_card = NULL; + } + + tpacpi_volume_checkpoint_nvram(); + + if (software_mute_active) + volume_exit_software_mute(); +} + +static int __init volume_create_alsa_mixer(void) +{ + struct snd_card *card; + struct tpacpi_alsa_data *data; + struct snd_kcontrol *ctl_vol; + struct snd_kcontrol *ctl_mute; + int rc; + + rc = snd_card_new(&tpacpi_pdev->dev, + alsa_index, alsa_id, THIS_MODULE, + sizeof(struct tpacpi_alsa_data), &card); + if (rc < 0 || !card) { + pr_err("Failed to create ALSA card structures: %d\n", rc); + return -ENODEV; + } + + BUG_ON(!card->private_data); + data = card->private_data; + data->card = card; + + strscpy(card->driver, TPACPI_ALSA_DRVNAME); + strscpy(card->shortname, TPACPI_ALSA_SHRTNAME); + snprintf(card->mixername, sizeof(card->mixername), "ThinkPad EC %s", + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "(unknown)"); + snprintf(card->longname, sizeof(card->longname), + "%s at EC reg 0x%02x, fw %s", card->shortname, TP_EC_AUDIO, + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "unknown"); + + if (volume_control_allowed) { + volume_alsa_control_vol.put = volume_alsa_vol_put; + volume_alsa_control_vol.access = + SNDRV_CTL_ELEM_ACCESS_READWRITE; + + volume_alsa_control_mute.put = volume_alsa_mute_put; + volume_alsa_control_mute.access = + SNDRV_CTL_ELEM_ACCESS_READWRITE; + } + + if (!tp_features.mixer_no_level_control) { + ctl_vol = snd_ctl_new1(&volume_alsa_control_vol, NULL); + rc = snd_ctl_add(card, ctl_vol); + if (rc < 0) { + pr_err("Failed to create ALSA volume control: %d\n", + rc); + goto err_exit; + } + data->ctl_vol_id = &ctl_vol->id; + } + + ctl_mute = snd_ctl_new1(&volume_alsa_control_mute, NULL); + rc = snd_ctl_add(card, ctl_mute); + if (rc < 0) { + pr_err("Failed to create ALSA mute control: %d\n", rc); + goto err_exit; + } + data->ctl_mute_id = &ctl_mute->id; + + rc = snd_card_register(card); + if (rc < 0) { + pr_err("Failed to register ALSA card: %d\n", rc); + goto err_exit; + } + + alsa_card = card; + return 0; + +err_exit: + snd_card_free(card); + return -ENODEV; +} + +#define TPACPI_VOL_Q_MUTEONLY 0x0001 /* Mute-only control available */ +#define TPACPI_VOL_Q_LEVEL 0x0002 /* Volume control available */ + +static const struct tpacpi_quirk volume_quirk_table[] __initconst = { + /* Whitelist volume level on all IBM by default */ + { .vendor = PCI_VENDOR_ID_IBM, + .bios = TPACPI_MATCH_ANY, + .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_VOL_Q_LEVEL }, + + /* Lenovo models with volume control (needs confirmation) */ + TPACPI_QEC_LNV('7', 'C', TPACPI_VOL_Q_LEVEL), /* R60/i */ + TPACPI_QEC_LNV('7', 'E', TPACPI_VOL_Q_LEVEL), /* R60e/i */ + TPACPI_QEC_LNV('7', '9', TPACPI_VOL_Q_LEVEL), /* T60/p */ + TPACPI_QEC_LNV('7', 'B', TPACPI_VOL_Q_LEVEL), /* X60/s */ + TPACPI_QEC_LNV('7', 'J', TPACPI_VOL_Q_LEVEL), /* X60t */ + TPACPI_QEC_LNV('7', '7', TPACPI_VOL_Q_LEVEL), /* Z60 */ + TPACPI_QEC_LNV('7', 'F', TPACPI_VOL_Q_LEVEL), /* Z61 */ + + /* Whitelist mute-only on all Lenovo by default */ + { .vendor = PCI_VENDOR_ID_LENOVO, + .bios = TPACPI_MATCH_ANY, + .ec = TPACPI_MATCH_ANY, + .quirks = TPACPI_VOL_Q_MUTEONLY } +}; + +static int __init volume_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + int rc; + + vdbg_printk(TPACPI_DBG_INIT, "initializing volume subdriver\n"); + + mutex_init(&volume_mutex); + + /* + * Check for module parameter bogosity, note that we + * init volume_mode to TPACPI_VOL_MODE_MAX in order to be + * able to detect "unspecified" + */ + if (volume_mode > TPACPI_VOL_MODE_MAX) + return -EINVAL; + + if (volume_mode == TPACPI_VOL_MODE_UCMS_STEP) { + pr_err("UCMS step volume mode not implemented, please contact %s\n", + TPACPI_MAIL); + return -ENODEV; + } + + if (volume_capabilities >= TPACPI_VOL_CAP_MAX) + return -EINVAL; + + /* + * The ALSA mixer is our primary interface. + * When disabled, don't install the subdriver at all + */ + if (!alsa_enable) { + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "ALSA mixer disabled by parameter, not loading volume subdriver...\n"); + return -ENODEV; + } + + quirks = tpacpi_check_quirks(volume_quirk_table, + ARRAY_SIZE(volume_quirk_table)); + + switch (volume_capabilities) { + case TPACPI_VOL_CAP_AUTO: + if (quirks & TPACPI_VOL_Q_MUTEONLY) + tp_features.mixer_no_level_control = 1; + else if (quirks & TPACPI_VOL_Q_LEVEL) + tp_features.mixer_no_level_control = 0; + else + return -ENODEV; /* no mixer */ + break; + case TPACPI_VOL_CAP_VOLMUTE: + tp_features.mixer_no_level_control = 0; + break; + case TPACPI_VOL_CAP_MUTEONLY: + tp_features.mixer_no_level_control = 1; + break; + default: + return -ENODEV; + } + + if (volume_capabilities != TPACPI_VOL_CAP_AUTO) + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "using user-supplied volume_capabilities=%d\n", + volume_capabilities); + + if (volume_mode == TPACPI_VOL_MODE_AUTO || + volume_mode == TPACPI_VOL_MODE_MAX) { + volume_mode = TPACPI_VOL_MODE_ECNVRAM; + + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "driver auto-selected volume_mode=%d\n", + volume_mode); + } else { + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "using user-supplied volume_mode=%d\n", + volume_mode); + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "mute is supported, volume control is %s\n", + str_supported(!tp_features.mixer_no_level_control)); + + if (software_mute_requested && volume_set_software_mute(true) == 0) { + software_mute_active = true; + } else { + rc = volume_create_alsa_mixer(); + if (rc) { + pr_err("Could not create the ALSA mixer interface\n"); + return rc; + } + + pr_info("Console audio control enabled, mode: %s\n", + (volume_control_allowed) ? + "override (read/write)" : + "monitor (read only)"); + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, + "registering volume hotkeys as change notification\n"); + tpacpi_hotkey_driver_mask_set(hotkey_driver_mask + | TP_ACPI_HKEY_VOLUP_MASK + | TP_ACPI_HKEY_VOLDWN_MASK + | TP_ACPI_HKEY_MUTE_MASK); + + return 0; +} + +static int volume_read(struct seq_file *m) +{ + u8 status; + + if (volume_get_status(&status) < 0) { + seq_printf(m, "level:\t\tunreadable\n"); + } else { + if (tp_features.mixer_no_level_control) + seq_printf(m, "level:\t\tunsupported\n"); + else + seq_printf(m, "level:\t\t%d\n", + status & TP_EC_AUDIO_LVL_MSK); + + seq_printf(m, "mute:\t\t%s\n", str_on_off(status & BIT(TP_EC_AUDIO_MUTESW))); + + if (volume_control_allowed) { + seq_printf(m, "commands:\tunmute, mute\n"); + if (!tp_features.mixer_no_level_control) { + seq_printf(m, "commands:\tup, down\n"); + seq_printf(m, "commands:\tlevel ( is 0-%d)\n", + TP_EC_VOLUME_MAX); + } + } + } + + return 0; +} + +static int volume_write(char *buf) +{ + u8 s; + u8 new_level, new_mute; + int l; + char *cmd; + int rc; + + /* + * We do allow volume control at driver startup, so that the + * user can set initial state through the volume=... parameter hack. + */ + if (!volume_control_allowed && tpacpi_lifecycle != TPACPI_LIFE_INIT) { + if (unlikely(!tp_warned.volume_ctrl_forbidden)) { + tp_warned.volume_ctrl_forbidden = 1; + pr_notice("Console audio control in monitor mode, changes are not allowed\n"); + pr_notice("Use the volume_control=1 module parameter to enable volume control\n"); + } + return -EPERM; + } + + rc = volume_get_status(&s); + if (rc < 0) + return rc; + + new_level = s & TP_EC_AUDIO_LVL_MSK; + new_mute = s & TP_EC_AUDIO_MUTESW_MSK; + + while ((cmd = strsep(&buf, ","))) { + if (!tp_features.mixer_no_level_control) { + if (strstarts(cmd, "up")) { + if (new_mute) + new_mute = 0; + else if (new_level < TP_EC_VOLUME_MAX) + new_level++; + continue; + } else if (strstarts(cmd, "down")) { + if (new_mute) + new_mute = 0; + else if (new_level > 0) + new_level--; + continue; + } else if (sscanf(cmd, "level %u", &l) == 1 && + l >= 0 && l <= TP_EC_VOLUME_MAX) { + new_level = l; + continue; + } + } + if (strstarts(cmd, "mute")) + new_mute = TP_EC_AUDIO_MUTESW_MSK; + else if (strstarts(cmd, "unmute")) + new_mute = 0; + else + return -EINVAL; + } + + if (tp_features.mixer_no_level_control) { + tpacpi_disclose_usertask("procfs volume", "%smute\n", + new_mute ? "" : "un"); + rc = volume_set_mute(!!new_mute); + } else { + tpacpi_disclose_usertask("procfs volume", + "%smute and set level to %d\n", + new_mute ? "" : "un", new_level); + rc = volume_set_status(new_mute | new_level); + } + volume_alsa_notify_change(); + + return (rc == -EINTR) ? -ERESTARTSYS : rc; +} + +static struct ibm_struct volume_driver_data = { + .name = "volume", + .read = volume_read, + .write = volume_write, + .exit = volume_exit, + .suspend = volume_suspend, + .resume = volume_resume, + .shutdown = volume_shutdown, +}; + +#else /* !CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +#define alsa_card NULL + +static inline void volume_alsa_notify_change(void) +{ +} + +static int __init volume_init(struct ibm_init_struct *iibm) +{ + pr_info("volume: disabled as there is no ALSA support in this kernel\n"); + + return -ENODEV; +} + +static struct ibm_struct volume_driver_data = { + .name = "volume", +}; + +#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +/************************************************************************* + * Fan subdriver + */ + +/* + * FAN ACCESS MODES + * + * TPACPI_FAN_RD_ACPI_GFAN: + * ACPI GFAN method: returns fan level + * + * see TPACPI_FAN_WR_ACPI_SFAN + * EC 0x2f (HFSP) not available if GFAN exists + * + * TPACPI_FAN_WR_ACPI_SFAN: + * ACPI SFAN method: sets fan level, 0 (stop) to 7 (max) + * + * EC 0x2f (HFSP) might be available *for reading*, but do not use + * it for writing. + * + * TPACPI_FAN_RD_ACPI_FANG: + * ACPI FANG method: returns fan control register + * + * Takes one parameter which is 0x8100 plus the offset to EC memory + * address 0xf500 and returns the byte at this address. + * + * 0xf500: + * When the value is less than 9 automatic mode is enabled + * 0xf502: + * Contains the current fan speed from 0-100% + * 0xf506: + * Bit 7 has to be set in order to enable manual control by + * writing a value >= 9 to 0xf500 + * + * TPACPI_FAN_WR_ACPI_FANW: + * ACPI FANW method: sets fan control registers + * + * Takes 0x8100 plus the offset to EC memory address 0xf500 and the + * value to be written there as parameters. + * + * see TPACPI_FAN_RD_ACPI_FANG + * + * TPACPI_FAN_WR_TPEC: + * ThinkPad EC register 0x2f (HFSP): fan control loop mode + * Supported on almost all ThinkPads + * + * Fan speed changes of any sort (including those caused by the + * disengaged mode) are usually done slowly by the firmware as the + * maximum amount of fan duty cycle change per second seems to be + * limited. + * + * Reading is not available if GFAN exists. + * Writing is not available if SFAN exists. + * + * Bits + * 7 automatic mode engaged; + * (default operation mode of the ThinkPad) + * fan level is ignored in this mode. + * 6 full speed mode (takes precedence over bit 7); + * not available on all thinkpads. May disable + * the tachometer while the fan controller ramps up + * the speed (which can take up to a few *minutes*). + * Speeds up fan to 100% duty-cycle, which is far above + * the standard RPM levels. It is not impossible that + * it could cause hardware damage. + * 5-3 unused in some models. Extra bits for fan level + * in others, but still useless as all values above + * 7 map to the same speed as level 7 in these models. + * 2-0 fan level (0..7 usually) + * 0x00 = stop + * 0x07 = max (set when temperatures critical) + * Some ThinkPads may have other levels, see + * TPACPI_FAN_WR_ACPI_FANS (X31/X40/X41) + * + * FIRMWARE BUG: on some models, EC 0x2f might not be initialized at + * boot. Apparently the EC does not initialize it, so unless ACPI DSDT + * does so, its initial value is meaningless (0x07). + * + * For firmware bugs, refer to: + * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * ---- + * + * ThinkPad EC register 0x84 (LSB), 0x85 (MSB): + * Main fan tachometer reading (in RPM) + * + * This register is present on all ThinkPads with a new-style EC, and + * it is known not to be present on the A21m/e, and T22, as there is + * something else in offset 0x84 according to the ACPI DSDT. Other + * ThinkPads from this same time period (and earlier) probably lack the + * tachometer as well. + * + * Unfortunately a lot of ThinkPads with new-style ECs but whose firmware + * was never fixed by IBM to report the EC firmware version string + * probably support the tachometer (like the early X models), so + * detecting it is quite hard. We need more data to know for sure. + * + * FIRMWARE BUG: always read 0x84 first, otherwise incorrect readings + * might result. + * + * FIRMWARE BUG: may go stale while the EC is switching to full speed + * mode. + * + * For firmware bugs, refer to: + * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues + * + * ---- + * + * ThinkPad EC register 0x31 bit 0 (only on select models) + * + * When bit 0 of EC register 0x31 is zero, the tachometer registers + * show the speed of the main fan. When bit 0 of EC register 0x31 + * is one, the tachometer registers show the speed of the auxiliary + * fan. + * + * Fan control seems to affect both fans, regardless of the state + * of this bit. + * + * So far, only the firmware for the X60/X61 non-tablet versions + * seem to support this (firmware TP-7M). + * + * TPACPI_FAN_WR_ACPI_FANS: + * ThinkPad X31, X40, X41. Not available in the X60. + * + * FANS ACPI handle: takes three arguments: low speed, medium speed, + * high speed. ACPI DSDT seems to map these three speeds to levels + * as follows: STOP LOW LOW MED MED HIGH HIGH HIGH HIGH + * (this map is stored on FAN0..FAN8 as "0,1,1,2,2,3,3,3,3") + * + * The speeds are stored on handles + * (FANA:FAN9), (FANC:FANB), (FANE:FAND). + * + * There are three default speed sets, accessible as handles: + * FS1L,FS1M,FS1H; FS2L,FS2M,FS2H; FS3L,FS3M,FS3H + * + * ACPI DSDT switches which set is in use depending on various + * factors. + * + * TPACPI_FAN_WR_TPEC is also available and should be used to + * command the fan. The X31/X40/X41 seems to have 8 fan levels, + * but the ACPI tables just mention level 7. + * + * TPACPI_FAN_RD_TPEC_NS: + * This mode is used for a few ThinkPads (L13 Yoga Gen2, X13 Yoga Gen2 etc.) + * that are using non-standard EC locations for reporting fan speeds. + * Currently these platforms only provide fan rpm reporting. + * + */ + +#define FAN_RPM_CAL_CONST 491520 /* FAN RPM calculation offset for some non-standard ECFW */ + +#define FAN_NS_CTRL_STATUS BIT(2) /* Bit which determines control is enabled or not */ +#define FAN_NS_CTRL BIT(4) /* Bit which determines control is by host or EC */ +#define FAN_CLOCK_TPM (22500*60) /* Ticks per minute for a 22.5 kHz clock */ + +enum { /* Fan control constants */ + fan_status_offset = 0x2f, /* EC register 0x2f */ + fan_rpm_offset = 0x84, /* EC register 0x84: LSB, 0x85 MSB (RPM) + * 0x84 must be read before 0x85 */ + fan_select_offset = 0x31, /* EC register 0x31 (Firmware 7M) + bit 0 selects which fan is active */ + + fan_status_offset_ns = 0x93, /* Special status/control offset for non-standard EC Fan1 */ + fan2_status_offset_ns = 0x96, /* Special status/control offset for non-standard EC Fan2 */ + fan_rpm_status_ns = 0x95, /* Special offset for Fan1 RPM status for non-standard EC */ + fan2_rpm_status_ns = 0x98, /* Special offset for Fan2 RPM status for non-standard EC */ + + TP_EC_FAN_FULLSPEED = 0x40, /* EC fan mode: full speed */ + TP_EC_FAN_AUTO = 0x80, /* EC fan mode: auto fan control */ + + TPACPI_FAN_LAST_LEVEL = 0x100, /* Use cached last-seen fan level */ +}; + +enum fan_status_access_mode { + TPACPI_FAN_NONE = 0, /* No fan status or control */ + TPACPI_FAN_RD_ACPI_GFAN, /* Use ACPI GFAN */ + TPACPI_FAN_RD_ACPI_FANG, /* Use ACPI FANG */ + TPACPI_FAN_RD_TPEC, /* Use ACPI EC regs 0x2f, 0x84-0x85 */ + TPACPI_FAN_RD_TPEC_NS, /* Use non-standard ACPI EC regs (eg: L13 Yoga gen2 etc.) */ +}; + +enum fan_control_access_mode { + TPACPI_FAN_WR_NONE = 0, /* No fan control */ + TPACPI_FAN_WR_ACPI_SFAN, /* Use ACPI SFAN */ + TPACPI_FAN_WR_ACPI_FANW, /* Use ACPI FANW */ + TPACPI_FAN_WR_TPEC, /* Use ACPI EC reg 0x2f */ + TPACPI_FAN_WR_ACPI_FANS, /* Use ACPI FANS and EC reg 0x2f */ +}; + +enum fan_control_commands { + TPACPI_FAN_CMD_SPEED = 0x0001, /* speed command */ + TPACPI_FAN_CMD_LEVEL = 0x0002, /* level command */ + TPACPI_FAN_CMD_ENABLE = 0x0004, /* enable/disable cmd, + * and also watchdog cmd */ +}; + +static bool fan_control_allowed; + +static enum fan_status_access_mode fan_status_access_mode; +static enum fan_control_access_mode fan_control_access_mode; +static enum fan_control_commands fan_control_commands; + +static u8 fan_control_initial_status; +static u8 fan_control_desired_level; +static u8 fan_control_resume_level; +static int fan_watchdog_maxinterval; + +static bool fan_with_ns_addr; +static bool ecfw_with_fan_dec_rpm; +static bool fan_speed_in_tpr; + +static struct mutex fan_mutex; + +static void fan_watchdog_fire(struct work_struct *ignored); +static DECLARE_DELAYED_WORK(fan_watchdog_task, fan_watchdog_fire); + +TPACPI_HANDLE(fans, ec, "FANS"); /* X31, X40, X41 */ +TPACPI_HANDLE(gfan, ec, "GFAN", /* 570 */ + "\\FSPD", /* 600e/x, 770e, 770x */ + ); /* all others */ +TPACPI_HANDLE(fang, ec, "FANG", /* E531 */ + ); /* all others */ +TPACPI_HANDLE(sfan, ec, "SFAN", /* 570 */ + "JFNS", /* 770x-JL */ + ); /* all others */ +TPACPI_HANDLE(fanw, ec, "FANW", /* E531 */ + ); /* all others */ + +/* + * Unitialized HFSP quirk: ACPI DSDT and EC fail to initialize the + * HFSP register at boot, so it contains 0x07 but the Thinkpad could + * be in auto mode (0x80). + * + * This is corrected by any write to HFSP either by the driver, or + * by the firmware. + * + * We assume 0x07 really means auto mode while this quirk is active, + * as this is far more likely than the ThinkPad being in level 7, + * which is only used by the firmware during thermal emergencies. + * + * Enable for TP-1Y (T43), TP-78 (R51e), TP-76 (R52), + * TP-70 (T43, R52), which are known to be buggy. + */ + +static void fan_quirk1_setup(void) +{ + if (fan_control_initial_status == 0x07) { + pr_notice("fan_init: initial fan status is unknown, assuming it is in auto mode\n"); + tp_features.fan_ctrl_status_undef = 1; + } +} + +static void fan_quirk1_handle(u8 *fan_status) +{ + if (unlikely(tp_features.fan_ctrl_status_undef)) { + if (*fan_status != fan_control_initial_status) { + /* something changed the HFSP regisnter since + * driver init time, so it is not undefined + * anymore */ + tp_features.fan_ctrl_status_undef = 0; + } else { + /* Return most likely status. In fact, it + * might be the only possible status */ + *fan_status = TP_EC_FAN_AUTO; + } + } +} + +/* Select main fan on X60/X61, NOOP on others */ +static bool fan_select_fan1(void) +{ + if (tp_features.second_fan) { + u8 val; + + if (ec_read(fan_select_offset, &val) < 0) + return false; + val &= 0xFEU; + if (ec_write(fan_select_offset, val) < 0) + return false; + } + return true; +} + +/* Select secondary fan on X60/X61 */ +static bool fan_select_fan2(void) +{ + u8 val; + + if (!tp_features.second_fan) + return false; + + if (ec_read(fan_select_offset, &val) < 0) + return false; + val |= 0x01U; + if (ec_write(fan_select_offset, val) < 0) + return false; + + return true; +} + +static void fan_update_desired_level(u8 status) +{ + lockdep_assert_held(&fan_mutex); + + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + if (status > 7) + fan_control_desired_level = 7; + else + fan_control_desired_level = status; + } +} + +static int fan_get_status(u8 *status) +{ + u8 s; + + /* TODO: + * Add TPACPI_FAN_RD_ACPI_FANS ? */ + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: { + /* 570, 600e/x, 770e, 770x */ + int res; + + if (unlikely(!acpi_evalf(gfan_handle, &res, NULL, "d"))) + return -EIO; + + if (likely(status)) + *status = res & 0x07; + + break; + } + case TPACPI_FAN_RD_ACPI_FANG: { + /* E531 */ + int mode, speed; + + if (unlikely(!acpi_evalf(fang_handle, &mode, NULL, "dd", 0x8100))) + return -EIO; + if (unlikely(!acpi_evalf(fang_handle, &speed, NULL, "dd", 0x8102))) + return -EIO; + + if (likely(status)) { + *status = speed * 7 / 100; + if (mode < 9) + *status |= TP_EC_FAN_AUTO; + } + + break; + } + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!acpi_ec_read(fan_status_offset, &s))) + return -EIO; + + if (likely(status)) { + *status = s; + fan_quirk1_handle(status); + } + + break; + case TPACPI_FAN_RD_TPEC_NS: + /* Default mode is AUTO which means controlled by EC */ + if (!acpi_ec_read(fan_status_offset_ns, &s)) + return -EIO; + + if (status) + *status = s; + + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_get_status_safe(u8 *status) +{ + int rc; + u8 s; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + rc = fan_get_status(&s); + /* NS EC doesn't have register with level settings */ + if (!rc && !fan_with_ns_addr) + fan_update_desired_level(s); + mutex_unlock(&fan_mutex); + + if (rc) + return rc; + if (status) + *status = s; + + return 0; +} + +static int fan_get_speed(unsigned int *speed) +{ + u8 hi, lo; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!fan_select_fan1())) + return -EIO; + if (unlikely(!acpi_ec_read(fan_rpm_offset, &lo) || + !acpi_ec_read(fan_rpm_offset + 1, &hi))) + return -EIO; + + if (likely(speed)) { + *speed = (hi << 8) | lo; + if (fan_speed_in_tpr && *speed != 0) + *speed = FAN_CLOCK_TPM / *speed; + } + break; + case TPACPI_FAN_RD_TPEC_NS: + if (!acpi_ec_read(fan_rpm_status_ns, &lo)) + return -EIO; + + if (speed) + *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; + break; + + default: + return -ENXIO; + } + + return 0; +} + +static int fan2_get_speed(unsigned int *speed) +{ + u8 hi, lo, status; + bool rc; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_TPEC: + /* all except 570, 600e/x, 770e, 770x */ + if (unlikely(!fan_select_fan2())) + return -EIO; + rc = !acpi_ec_read(fan_rpm_offset, &lo) || + !acpi_ec_read(fan_rpm_offset + 1, &hi); + fan_select_fan1(); /* play it safe */ + if (rc) + return -EIO; + + if (likely(speed)) { + *speed = (hi << 8) | lo; + if (fan_speed_in_tpr && *speed != 0) + *speed = FAN_CLOCK_TPM / *speed; + } + break; + + case TPACPI_FAN_RD_TPEC_NS: + rc = !acpi_ec_read(fan2_status_offset_ns, &status); + if (rc) + return -EIO; + if (!(status & FAN_NS_CTRL_STATUS)) { + pr_info("secondary fan control not supported\n"); + return -EIO; + } + rc = !acpi_ec_read(fan2_rpm_status_ns, &lo); + if (rc) + return -EIO; + if (speed) + *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; + break; + case TPACPI_FAN_RD_ACPI_FANG: { + /* E531 */ + int speed_tmp; + + if (unlikely(!acpi_evalf(fang_handle, &speed_tmp, NULL, "dd", 0x8102))) + return -EIO; + + if (likely(speed)) + *speed = speed_tmp * 65535 / 100; + break; + } + + default: + return -ENXIO; + } + + return 0; +} + +static int fan_set_level(int level) +{ + if (!fan_control_allowed) + return -EPERM; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + if ((level < 0) || (level > 7)) + return -EINVAL; + + if (tp_features.second_fan_ctl) { + if (!fan_select_fan2() || + !acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) { + pr_warn("Couldn't set 2nd fan level, disabling support\n"); + tp_features.second_fan_ctl = 0; + } + fan_select_fan1(); + } + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) + return -EIO; + break; + + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!(level & TP_EC_FAN_AUTO) && + !(level & TP_EC_FAN_FULLSPEED) && + ((level < 0) || (level > 7))) + return -EINVAL; + + /* safety net should the EC not support AUTO + * or FULLSPEED mode bits and just ignore them */ + if (level & TP_EC_FAN_FULLSPEED) + level |= 7; /* safety min speed 7 */ + else if (level & TP_EC_FAN_AUTO) + level |= 4; /* safety min speed 4 */ + + if (tp_features.second_fan_ctl) { + if (!fan_select_fan2() || + !acpi_ec_write(fan_status_offset, level)) { + pr_warn("Couldn't set 2nd fan level, disabling support\n"); + tp_features.second_fan_ctl = 0; + } + fan_select_fan1(); + + } + if (!acpi_ec_write(fan_status_offset, level)) + return -EIO; + else + tp_features.fan_ctrl_status_undef = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!(level & TP_EC_FAN_AUTO) && (level < 0 || level > 7)) + return -EINVAL; + if (level & TP_EC_FAN_FULLSPEED) + return -EINVAL; + + if (level & TP_EC_FAN_AUTO) { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { + return -EIO; + } + } else { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + return -EIO; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, level * 100 / 7)) { + return -EIO; + } + } + break; + + default: + return -ENXIO; + } + + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0x%02x\n", level); + return 0; +} + +static int fan_set_level_safe(int level) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + if (level == TPACPI_FAN_LAST_LEVEL) + level = fan_control_desired_level; + + rc = fan_set_level(level); + if (!rc) + fan_update_desired_level(level); + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_enable(void) +{ + u8 s = 0; + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + rc = fan_get_status(&s); + if (rc) + break; + + /* Don't go out of emergency fan mode */ + if (s != 7) { + s &= 0x07; + s |= TP_EC_FAN_AUTO | 4; /* min fan speed 4 */ + } + + if (!acpi_ec_write(fan_status_offset, s)) + rc = -EIO; + else { + tp_features.fan_ctrl_status_undef = 0; + rc = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + rc = fan_get_status(&s); + if (rc) + break; + + s &= 0x07; + + /* Set fan to at least level 4 */ + s |= 4; + + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", s)) + rc = -EIO; + else + rc = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { + rc = -EIO; + break; + } + + rc = 0; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + + if (!rc) + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0x%02x\n", + s); + return rc; +} + +static int fan_set_disable(void) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + if (!acpi_ec_write(fan_status_offset, 0x00)) + rc = -EIO; + else { + fan_control_desired_level = 0; + tp_features.fan_ctrl_status_undef = 0; + } + break; + + case TPACPI_FAN_WR_ACPI_SFAN: + if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", 0x00)) + rc = -EIO; + else + fan_control_desired_level = 0; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, 0x00)) { + rc = -EIO; + break; + } + rc = 0; + break; + + default: + rc = -ENXIO; + } + + if (!rc) + vdbg_printk(TPACPI_DBG_FAN, + "fan control: set fan control register to 0\n"); + + mutex_unlock(&fan_mutex); + return rc; +} + +static int fan_set_speed(int speed) +{ + int rc; + + if (!fan_control_allowed) + return -EPERM; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = 0; + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_FANS: + if (speed >= 0 && speed <= 65535) { + if (!acpi_evalf(fans_handle, NULL, NULL, "vddd", + speed, speed, speed)) + rc = -EIO; + } else + rc = -EINVAL; + break; + + case TPACPI_FAN_WR_ACPI_FANW: + if (speed >= 0 && speed <= 65535) { + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { + rc = -EIO; + break; + } + if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", + 0x8102, speed * 100 / 65535)) + rc = -EIO; + } else + rc = -EINVAL; + break; + + default: + rc = -ENXIO; + } + + mutex_unlock(&fan_mutex); + return rc; +} + +static void fan_watchdog_reset(void) +{ + if (fan_control_access_mode == TPACPI_FAN_WR_NONE) + return; + + if (fan_watchdog_maxinterval > 0 && + tpacpi_lifecycle != TPACPI_LIFE_EXITING) + mod_delayed_work(tpacpi_wq, &fan_watchdog_task, + secs_to_jiffies(fan_watchdog_maxinterval)); + else + cancel_delayed_work(&fan_watchdog_task); +} + +static void fan_watchdog_fire(struct work_struct *ignored) +{ + int rc; + + if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) + return; + + pr_notice("fan watchdog: enabling fan\n"); + rc = fan_set_enable(); + if (rc < 0) { + pr_err("fan watchdog: error %d while enabling fan, will try again later...\n", + rc); + /* reschedule for later */ + fan_watchdog_reset(); + } +} + +/* + * SYSFS fan layout: hwmon compatible (device) + * + * pwm*_enable: + * 0: "disengaged" mode + * 1: manual mode + * 2: native EC "auto" mode (recommended, hardware default) + * + * pwm*: set speed in manual mode, ignored otherwise. + * 0 is level 0; 255 is level 7. Intermediate points done with linear + * interpolation. + * + * fan*_input: tachometer reading, RPM + * + * + * SYSFS fan layout: extensions + * + * fan_watchdog (driver): + * fan watchdog interval in seconds, 0 disables (default), max 120 + */ + +/* sysfs fan pwm1_enable ----------------------------------------------- */ +static ssize_t fan_pwm1_enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res, mode; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if (status & TP_EC_FAN_FULLSPEED) { + mode = 0; + } else if (status & TP_EC_FAN_AUTO) { + mode = 2; + } else + mode = 1; + + return sysfs_emit(buf, "%d\n", mode); +} + +static ssize_t fan_pwm1_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long t; + int res, level; + + if (parse_strtoul(buf, 2, &t)) + return -EINVAL; + + tpacpi_disclose_usertask("hwmon pwm1_enable", + "set fan mode to %lu\n", t); + + switch (t) { + case 0: + level = TP_EC_FAN_FULLSPEED; + break; + case 1: + level = TPACPI_FAN_LAST_LEVEL; + break; + case 2: + level = TP_EC_FAN_AUTO; + break; + case 3: + /* reserved for software-controlled auto mode */ + return -ENOSYS; + default: + return -EINVAL; + } + + res = fan_set_level_safe(level); + if (res == -ENXIO) + return -EINVAL; + else if (res < 0) + return res; + + fan_watchdog_reset(); + + return count; +} + +static DEVICE_ATTR(pwm1_enable, S_IWUSR | S_IRUGO, + fan_pwm1_enable_show, fan_pwm1_enable_store); + +/* sysfs fan pwm1 ------------------------------------------------------ */ +static ssize_t fan_pwm1_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + u8 status; + + res = fan_get_status_safe(&status); + if (res) + return res; + + if ((status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) != 0) + status = fan_control_desired_level; + + if (status > 7) + status = 7; + + return sysfs_emit(buf, "%u\n", (status * 255) / 7); +} + +static ssize_t fan_pwm1_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long s; + int rc; + u8 status, newlevel; + + if (parse_strtoul(buf, 255, &s)) + return -EINVAL; + + tpacpi_disclose_usertask("hwmon pwm1", + "set fan speed to %lu\n", s); + + /* scale down from 0-255 to 0-7 */ + newlevel = (s >> 5) & 0x07; + + if (mutex_lock_killable(&fan_mutex)) + return -ERESTARTSYS; + + rc = fan_get_status(&status); + if (!rc && (status & + (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { + rc = fan_set_level(newlevel); + if (rc == -ENXIO) + rc = -EINVAL; + else if (!rc) { + fan_update_desired_level(newlevel); + fan_watchdog_reset(); + } + } + + mutex_unlock(&fan_mutex); + return (rc) ? rc : count; +} + +static DEVICE_ATTR(pwm1, S_IWUSR | S_IRUGO, fan_pwm1_show, fan_pwm1_store); + +/* sysfs fan fan1_input ------------------------------------------------ */ +static ssize_t fan_fan1_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + unsigned int speed; + + res = fan_get_speed(&speed); + if (res < 0) + return res; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + return sysfs_emit(buf, "%u\n", speed); + else + return sysfs_emit(buf, "%x\n", speed); +} + +static DEVICE_ATTR(fan1_input, S_IRUGO, fan_fan1_input_show, NULL); + +/* sysfs fan fan2_input ------------------------------------------------ */ +static ssize_t fan_fan2_input_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int res; + unsigned int speed; + + res = fan2_get_speed(&speed); + if (res < 0) + return res; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + return sysfs_emit(buf, "%u\n", speed); + else + return sysfs_emit(buf, "%x\n", speed); +} + +static DEVICE_ATTR(fan2_input, S_IRUGO, fan_fan2_input_show, NULL); + +/* sysfs fan fan_watchdog (hwmon driver) ------------------------------- */ +static ssize_t fan_watchdog_show(struct device_driver *drv, char *buf) +{ + return sysfs_emit(buf, "%u\n", fan_watchdog_maxinterval); +} + +static ssize_t fan_watchdog_store(struct device_driver *drv, const char *buf, + size_t count) +{ + unsigned long t; + + if (parse_strtoul(buf, 120, &t)) + return -EINVAL; + + if (!fan_control_allowed) + return -EPERM; + + fan_watchdog_maxinterval = t; + fan_watchdog_reset(); + + tpacpi_disclose_usertask("fan_watchdog", "set to %lu\n", t); + + return count; +} +static DRIVER_ATTR_RW(fan_watchdog); + +/* --------------------------------------------------------------------- */ + +static struct attribute *fan_attributes[] = { + &dev_attr_pwm1_enable.attr, + &dev_attr_pwm1.attr, + &dev_attr_fan1_input.attr, + &dev_attr_fan2_input.attr, + NULL +}; + +static umode_t fan_attr_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + if (fan_status_access_mode == TPACPI_FAN_NONE && + fan_control_access_mode == TPACPI_FAN_WR_NONE) + return 0; + + if (attr == &dev_attr_fan2_input.attr) { + if (!tp_features.second_fan) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group fan_attr_group = { + .is_visible = fan_attr_is_visible, + .attrs = fan_attributes, +}; + +static struct attribute *fan_driver_attributes[] = { + &driver_attr_fan_watchdog.attr, + NULL +}; + +static const struct attribute_group fan_driver_attr_group = { + .is_visible = fan_attr_is_visible, + .attrs = fan_driver_attributes, +}; + +#define TPACPI_FAN_Q1 0x0001 /* Uninitialized HFSP */ +#define TPACPI_FAN_2FAN 0x0002 /* EC 0x31 bit 0 selects fan2 */ +#define TPACPI_FAN_2CTL 0x0004 /* selects fan2 control */ +#define TPACPI_FAN_NOFAN 0x0008 /* no fan available */ +#define TPACPI_FAN_NS 0x0010 /* For EC with non-Standard register addresses */ +#define TPACPI_FAN_DECRPM 0x0020 /* For ECFW's with RPM in register as decimal */ +#define TPACPI_FAN_TPR 0x0040 /* Fan speed is in Ticks Per Revolution */ +#define TPACPI_FAN_NOACPI 0x0080 /* Don't use ACPI methods even if detected */ + +static const struct tpacpi_quirk fan_quirk_table[] __initconst = { + TPACPI_QEC_IBM('1', 'Y', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '8', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '6', TPACPI_FAN_Q1), + TPACPI_QEC_IBM('7', '0', TPACPI_FAN_Q1), + TPACPI_QEC_LNV('7', 'M', TPACPI_FAN_2FAN), + TPACPI_Q_LNV('N', '1', TPACPI_FAN_2FAN), + TPACPI_Q_LNV3('N', '1', 'D', TPACPI_FAN_2CTL), /* P70 */ + TPACPI_Q_LNV3('N', '1', 'E', TPACPI_FAN_2CTL), /* P50 */ + TPACPI_Q_LNV3('N', '1', 'T', TPACPI_FAN_2CTL), /* P71 */ + TPACPI_Q_LNV3('N', '1', 'U', TPACPI_FAN_2CTL), /* P51 */ + TPACPI_Q_LNV3('N', '2', 'C', TPACPI_FAN_2CTL), /* P52 / P72 */ + TPACPI_Q_LNV3('N', '2', 'N', TPACPI_FAN_2CTL), /* P53 / P73 */ + TPACPI_Q_LNV3('N', '2', 'E', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (1st gen) */ + TPACPI_Q_LNV3('N', '2', 'O', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (2nd gen) */ + TPACPI_Q_LNV3('N', '3', '0', TPACPI_FAN_2CTL), /* P15 (1st gen) / P15v (1st gen) */ + TPACPI_Q_LNV3('N', '3', '7', TPACPI_FAN_2CTL), /* T15g (2nd gen) */ + TPACPI_Q_LNV3('R', '1', 'F', TPACPI_FAN_NS), /* L13 Yoga Gen 2 */ + TPACPI_Q_LNV3('N', '2', 'U', TPACPI_FAN_NS), /* X13 Yoga Gen 2*/ + TPACPI_Q_LNV3('R', '0', 'R', TPACPI_FAN_NS), /* L380 */ + TPACPI_Q_LNV3('R', '1', '5', TPACPI_FAN_NS), /* L13 Yoga Gen 1 */ + TPACPI_Q_LNV3('R', '1', '0', TPACPI_FAN_NS), /* L390 */ + TPACPI_Q_LNV3('N', '2', 'L', TPACPI_FAN_NS), /* X13 Yoga Gen 1 */ + TPACPI_Q_LNV3('R', '0', 'T', TPACPI_FAN_NS), /* 11e Gen5 GL */ + TPACPI_Q_LNV3('R', '1', 'D', TPACPI_FAN_NS), /* 11e Gen5 GL-R */ + TPACPI_Q_LNV3('R', '0', 'V', TPACPI_FAN_NS), /* 11e Gen5 KL-Y */ + TPACPI_Q_LNV3('N', '1', 'O', TPACPI_FAN_NOFAN), /* X1 Tablet (2nd gen) */ + TPACPI_Q_LNV3('R', '0', 'Q', TPACPI_FAN_DECRPM),/* L480 */ + TPACPI_Q_LNV('8', 'F', TPACPI_FAN_TPR), /* ThinkPad x120e */ + TPACPI_Q_LNV3('R', '0', '0', TPACPI_FAN_NOACPI),/* E560 */ + TPACPI_Q_LNV3('R', '1', '2', TPACPI_FAN_NOACPI),/* T495 */ + TPACPI_Q_LNV3('R', '1', '3', TPACPI_FAN_NOACPI),/* T495s */ +}; + +static int __init fan_init(struct ibm_init_struct *iibm) +{ + unsigned long quirks; + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "initializing fan subdriver\n"); + + mutex_init(&fan_mutex); + fan_status_access_mode = TPACPI_FAN_NONE; + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + fan_watchdog_maxinterval = 0; + tp_features.fan_ctrl_status_undef = 0; + tp_features.second_fan = 0; + tp_features.second_fan_ctl = 0; + fan_control_desired_level = 7; + + if (tpacpi_is_ibm()) { + TPACPI_ACPIHANDLE_INIT(fans); + TPACPI_ACPIHANDLE_INIT(gfan); + TPACPI_ACPIHANDLE_INIT(sfan); + } + if (tpacpi_is_lenovo()) { + TPACPI_ACPIHANDLE_INIT(fang); + TPACPI_ACPIHANDLE_INIT(fanw); + } + + quirks = tpacpi_check_quirks(fan_quirk_table, + ARRAY_SIZE(fan_quirk_table)); + + if (quirks & TPACPI_FAN_NOFAN) { + pr_info("No integrated ThinkPad fan available\n"); + return -ENODEV; + } + + if (quirks & TPACPI_FAN_NS) { + pr_info("ECFW with non-standard fan reg control found\n"); + fan_with_ns_addr = 1; + /* Fan ctrl support from host is undefined for now */ + tp_features.fan_ctrl_status_undef = 1; + } + + /* Check for the EC/BIOS with RPM reported in decimal*/ + if (quirks & TPACPI_FAN_DECRPM) { + pr_info("ECFW with fan RPM as decimal in EC register\n"); + ecfw_with_fan_dec_rpm = 1; + tp_features.fan_ctrl_status_undef = 1; + } + + if (quirks & TPACPI_FAN_NOACPI) { + /* E560, T495, T495s */ + pr_info("Ignoring buggy ACPI fan access method\n"); + fang_handle = NULL; + fanw_handle = NULL; + } + + if (gfan_handle) { + /* 570, 600e/x, 770e, 770x */ + fan_status_access_mode = TPACPI_FAN_RD_ACPI_GFAN; + } else if (fang_handle) { + /* E531 */ + fan_status_access_mode = TPACPI_FAN_RD_ACPI_FANG; + } else { + /* all other ThinkPads: note that even old-style + * ThinkPad ECs supports the fan control register */ + if (fan_with_ns_addr || + likely(acpi_ec_read(fan_status_offset, &fan_control_initial_status))) { + int res; + unsigned int speed; + + fan_status_access_mode = fan_with_ns_addr ? + TPACPI_FAN_RD_TPEC_NS : TPACPI_FAN_RD_TPEC; + + if (quirks & TPACPI_FAN_Q1) + fan_quirk1_setup(); + if (quirks & TPACPI_FAN_TPR) + fan_speed_in_tpr = true; + /* Try and probe the 2nd fan */ + tp_features.second_fan = 1; /* needed for get_speed to work */ + res = fan2_get_speed(&speed); + if (res >= 0 && speed != FAN_NOT_PRESENT) { + /* It responded - so let's assume it's there */ + tp_features.second_fan = 1; + /* fan control not currently available for ns ECFW */ + tp_features.second_fan_ctl = !fan_with_ns_addr; + pr_info("secondary fan control detected & enabled\n"); + } else { + /* Fan not auto-detected */ + tp_features.second_fan = 0; + if (quirks & TPACPI_FAN_2FAN) { + tp_features.second_fan = 1; + pr_info("secondary fan support enabled\n"); + } + if (quirks & TPACPI_FAN_2CTL) { + tp_features.second_fan = 1; + tp_features.second_fan_ctl = 1; + pr_info("secondary fan control enabled\n"); + } + } + } else { + pr_err("ThinkPad ACPI EC access misbehaving, fan status and control unavailable\n"); + return -ENODEV; + } + } + + if (sfan_handle) { + /* 570, 770x-JL */ + fan_control_access_mode = TPACPI_FAN_WR_ACPI_SFAN; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_ENABLE; + } else if (fanw_handle) { + /* E531 */ + fan_control_access_mode = TPACPI_FAN_WR_ACPI_FANW; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_SPEED | TPACPI_FAN_CMD_ENABLE; + } else { + if (!gfan_handle) { + /* gfan without sfan means no fan control */ + /* all other models implement TP EC 0x2f control */ + + if (fans_handle) { + /* X31, X40, X41 */ + fan_control_access_mode = + TPACPI_FAN_WR_ACPI_FANS; + fan_control_commands |= + TPACPI_FAN_CMD_SPEED | + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } else { + fan_control_access_mode = TPACPI_FAN_WR_TPEC; + fan_control_commands |= + TPACPI_FAN_CMD_LEVEL | + TPACPI_FAN_CMD_ENABLE; + } + } + } + + vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "fan is %s, modes %d, %d\n", + str_supported(fan_status_access_mode != TPACPI_FAN_NONE || + fan_control_access_mode != TPACPI_FAN_WR_NONE), + fan_status_access_mode, fan_control_access_mode); + + /* fan control master switch */ + if (!fan_control_allowed) { + fan_control_access_mode = TPACPI_FAN_WR_NONE; + fan_control_commands = 0; + dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, + "fan control features disabled by parameter\n"); + } + + /* update fan_control_desired_level */ + if (fan_status_access_mode != TPACPI_FAN_NONE) + fan_get_status_safe(NULL); + + if (fan_status_access_mode == TPACPI_FAN_NONE && + fan_control_access_mode == TPACPI_FAN_WR_NONE) + return -ENODEV; + + return 0; +} + +static void fan_exit(void) +{ + vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_FAN, + "cancelling any pending fan watchdog tasks\n"); + + cancel_delayed_work(&fan_watchdog_task); + flush_workqueue(tpacpi_wq); +} + +static void fan_suspend(void) +{ + int rc; + + if (!fan_control_allowed) + return; + + /* Store fan status in cache */ + fan_control_resume_level = 0; + rc = fan_get_status_safe(&fan_control_resume_level); + if (rc) + pr_notice("failed to read fan level for later restore during resume: %d\n", + rc); + + /* if it is undefined, don't attempt to restore it. + * KEEP THIS LAST */ + if (tp_features.fan_ctrl_status_undef) + fan_control_resume_level = 0; +} + +static void fan_resume(void) +{ + u8 current_level = 7; + bool do_set = false; + int rc; + + /* DSDT *always* updates status on resume */ + tp_features.fan_ctrl_status_undef = 0; + + if (!fan_control_allowed || + !fan_control_resume_level || + fan_get_status_safe(¤t_level)) + return; + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + /* never decrease fan level */ + do_set = (fan_control_resume_level > current_level); + break; + case TPACPI_FAN_WR_ACPI_FANS: + case TPACPI_FAN_WR_TPEC: + /* never decrease fan level, scale is: + * TP_EC_FAN_FULLSPEED > 7 >= TP_EC_FAN_AUTO + * + * We expect the firmware to set either 7 or AUTO, but we + * handle FULLSPEED out of paranoia. + * + * So, we can safely only restore FULLSPEED or 7, anything + * else could slow the fan. Restoring AUTO is useless, at + * best that's exactly what the DSDT already set (it is the + * slower it uses). + * + * Always keep in mind that the DSDT *will* have set the + * fans to what the vendor supposes is the best level. We + * muck with it only to speed the fan up. + */ + if (fan_control_resume_level != 7 && + !(fan_control_resume_level & TP_EC_FAN_FULLSPEED)) + return; + else + do_set = !(current_level & TP_EC_FAN_FULLSPEED) && + (current_level != fan_control_resume_level); + break; + default: + return; + } + if (do_set) { + pr_notice("restoring fan level to 0x%02x\n", + fan_control_resume_level); + rc = fan_set_level_safe(fan_control_resume_level); + if (rc < 0) + pr_notice("failed to restore fan level: %d\n", rc); + } +} + +static int fan_read(struct seq_file *m) +{ + int rc; + u8 status; + unsigned int speed = 0; + + switch (fan_status_access_mode) { + case TPACPI_FAN_RD_ACPI_GFAN: + /* 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc) + return rc; + + seq_printf(m, "status:\t\t%s\n" + "level:\t\t%d\n", + str_enabled_disabled(status), status); + break; + + case TPACPI_FAN_RD_TPEC_NS: + case TPACPI_FAN_RD_TPEC: + case TPACPI_FAN_RD_ACPI_FANG: + /* all except 570, 600e/x, 770e, 770x */ + rc = fan_get_status_safe(&status); + if (rc) + return rc; + + seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status)); + + rc = fan_get_speed(&speed); + if (rc < 0) + return rc; + + /* Check for fan speeds displayed in hexadecimal */ + if (!ecfw_with_fan_dec_rpm) + seq_printf(m, "speed:\t\t%d\n", speed); + else + seq_printf(m, "speed:\t\t%x\n", speed); + + if (fan_status_access_mode == TPACPI_FAN_RD_TPEC_NS) { + /* + * No full speed bit in NS EC + * EC Auto mode is set by default. + * No other levels settings available + */ + seq_printf(m, "level:\t\t%s\n", status & FAN_NS_CTRL ? "unknown" : "auto"); + } else if (fan_status_access_mode == TPACPI_FAN_RD_TPEC) { + if (status & TP_EC_FAN_FULLSPEED) + /* Disengaged mode takes precedence */ + seq_printf(m, "level:\t\tdisengaged\n"); + else if (status & TP_EC_FAN_AUTO) + seq_printf(m, "level:\t\tauto\n"); + else + seq_printf(m, "level:\t\t%d\n", status); + } + break; + + case TPACPI_FAN_NONE: + default: + seq_printf(m, "status:\t\tnot supported\n"); + } + + if (fan_control_commands & TPACPI_FAN_CMD_LEVEL) { + seq_printf(m, "commands:\tlevel "); + + switch (fan_control_access_mode) { + case TPACPI_FAN_WR_ACPI_SFAN: + seq_printf(m, " ( is 0-7)\n"); + break; + + default: + seq_printf(m, " ( is 0-7, auto, disengaged, full-speed)\n"); + break; + } + } + + if (fan_control_commands & TPACPI_FAN_CMD_ENABLE) + seq_printf(m, "commands:\tenable, disable\n" + "commands:\twatchdog ( is 0 (off), 1-120 (seconds))\n"); + + if (fan_control_commands & TPACPI_FAN_CMD_SPEED) + seq_printf(m, "commands:\tspeed ( is 0-65535)\n"); + + return 0; +} + +static int fan_write_cmd_level(const char *cmd, int *rc) +{ + int level; + + if (strstarts(cmd, "level auto")) + level = TP_EC_FAN_AUTO; + else if (strstarts(cmd, "level disengaged") || strstarts(cmd, "level full-speed")) + level = TP_EC_FAN_FULLSPEED; + else if (sscanf(cmd, "level %d", &level) != 1) + return 0; + + *rc = fan_set_level_safe(level); + if (*rc == -ENXIO) + pr_err("level command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", + "set level to %d\n", level); + + return 1; +} + +static int fan_write_cmd_enable(const char *cmd, int *rc) +{ + if (!strstarts(cmd, "enable")) + return 0; + + *rc = fan_set_enable(); + if (*rc == -ENXIO) + pr_err("enable command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", "enable\n"); + + return 1; +} + +static int fan_write_cmd_disable(const char *cmd, int *rc) +{ + if (!strstarts(cmd, "disable")) + return 0; + + *rc = fan_set_disable(); + if (*rc == -ENXIO) + pr_err("disable command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", "disable\n"); + + return 1; +} + +static int fan_write_cmd_speed(const char *cmd, int *rc) +{ + int speed; + + /* TODO: + * Support speed ? */ + + if (sscanf(cmd, "speed %d", &speed) != 1) + return 0; + + *rc = fan_set_speed(speed); + if (*rc == -ENXIO) + pr_err("speed command accepted for unsupported access mode %d\n", + fan_control_access_mode); + else if (!*rc) + tpacpi_disclose_usertask("procfs fan", + "set speed to %d\n", speed); + + return 1; +} + +static int fan_write_cmd_watchdog(const char *cmd, int *rc) +{ + int interval; + + if (sscanf(cmd, "watchdog %d", &interval) != 1) + return 0; + + if (interval < 0 || interval > 120) + *rc = -EINVAL; + else { + fan_watchdog_maxinterval = interval; + tpacpi_disclose_usertask("procfs fan", + "set watchdog timer to %d\n", + interval); + } + + return 1; +} + +static int fan_write(char *buf) +{ + char *cmd; + int rc = 0; + + while (!rc && (cmd = strsep(&buf, ","))) { + if (!((fan_control_commands & TPACPI_FAN_CMD_LEVEL) && + fan_write_cmd_level(cmd, &rc)) && + !((fan_control_commands & TPACPI_FAN_CMD_ENABLE) && + (fan_write_cmd_enable(cmd, &rc) || + fan_write_cmd_disable(cmd, &rc) || + fan_write_cmd_watchdog(cmd, &rc))) && + !((fan_control_commands & TPACPI_FAN_CMD_SPEED) && + fan_write_cmd_speed(cmd, &rc)) + ) + rc = -EINVAL; + else if (!rc) + fan_watchdog_reset(); + } + + return rc; +} + +static struct ibm_struct fan_driver_data = { + .name = "fan", + .read = fan_read, + .write = fan_write, + .exit = fan_exit, + .suspend = fan_suspend, + .resume = fan_resume, +}; + +/************************************************************************* + * Mute LED subdriver + */ + +#define TPACPI_LED_MAX 2 + +struct tp_led_table { + acpi_string name; + int on_value; + int off_value; + int state; +}; + +static struct tp_led_table led_tables[TPACPI_LED_MAX] = { + [LED_AUDIO_MUTE] = { + .name = "SSMS", + .on_value = 1, + .off_value = 0, + }, + [LED_AUDIO_MICMUTE] = { + .name = "MMTS", + .on_value = 2, + .off_value = 0, + }, +}; + +static int mute_led_on_off(struct tp_led_table *t, bool state) +{ + acpi_handle temp; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { + pr_warn("Thinkpad ACPI has no %s interface.\n", t->name); + return -EIO; + } + + if (!acpi_evalf(hkey_handle, &output, t->name, "dd", + state ? t->on_value : t->off_value)) + return -EIO; + + t->state = state; + return state; +} + +static int tpacpi_led_set(int whichled, bool on) +{ + struct tp_led_table *t; + + t = &led_tables[whichled]; + if (t->state < 0 || t->state == on) + return t->state; + return mute_led_on_off(t, on); +} + +static int tpacpi_led_mute_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return tpacpi_led_set(LED_AUDIO_MUTE, brightness != LED_OFF); +} + +static int tpacpi_led_micmute_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + return tpacpi_led_set(LED_AUDIO_MICMUTE, brightness != LED_OFF); +} + +static struct led_classdev mute_led_cdev[TPACPI_LED_MAX] = { + [LED_AUDIO_MUTE] = { + .name = "platform::mute", + .max_brightness = 1, + .brightness_set_blocking = tpacpi_led_mute_set, + .default_trigger = "audio-mute", + }, + [LED_AUDIO_MICMUTE] = { + .name = "platform::micmute", + .max_brightness = 1, + .brightness_set_blocking = tpacpi_led_micmute_set, + .default_trigger = "audio-micmute", + }, +}; + +static int mute_led_init(struct ibm_init_struct *iibm) +{ + acpi_handle temp; + int i, err; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + struct tp_led_table *t = &led_tables[i]; + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { + t->state = -ENODEV; + continue; + } + + err = led_classdev_register(&tpacpi_pdev->dev, &mute_led_cdev[i]); + if (err < 0) { + while (i--) + led_classdev_unregister(&mute_led_cdev[i]); + return err; + } + } + return 0; +} + +static void mute_led_exit(void) +{ + int i; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + led_classdev_unregister(&mute_led_cdev[i]); + tpacpi_led_set(i, false); + } +} + +static void mute_led_resume(void) +{ + int i; + + for (i = 0; i < TPACPI_LED_MAX; i++) { + struct tp_led_table *t = &led_tables[i]; + if (t->state >= 0) + mute_led_on_off(t, t->state); + } +} + +static struct ibm_struct mute_led_driver_data = { + .name = "mute_led", + .exit = mute_led_exit, + .resume = mute_led_resume, +}; + +/* + * Battery Wear Control Driver + * Contact: Ognjen Galic + */ + +/* Metadata */ + +#define GET_START "BCTG" +#define SET_START "BCCS" +#define GET_STOP "BCSG" +#define SET_STOP "BCSS" +#define GET_DISCHARGE "BDSG" +#define SET_DISCHARGE "BDSS" +#define GET_INHIBIT "BICG" +#define SET_INHIBIT "BICS" + +enum { + BAT_ANY = 0, + BAT_PRIMARY = 1, + BAT_SECONDARY = 2 +}; + +enum { + /* Error condition bit */ + METHOD_ERR = BIT(31), +}; + +enum { + /* This is used in the get/set helpers */ + THRESHOLD_START, + THRESHOLD_STOP, + FORCE_DISCHARGE, + INHIBIT_CHARGE, +}; + +struct tpacpi_battery_data { + int charge_start; + int start_support; + int charge_stop; + int stop_support; + unsigned int charge_behaviours; +}; + +struct tpacpi_battery_driver_data { + struct tpacpi_battery_data batteries[3]; + int individual_addressing; +}; + +static struct tpacpi_battery_driver_data battery_info; + +/* ACPI helpers/functions/probes */ + +/* + * This evaluates a ACPI method call specific to the battery + * ACPI extension. The specifics are that an error is marked + * in the 32rd bit of the response, so we just check that here. + */ +static acpi_status tpacpi_battery_acpi_eval(char *method, int *ret, int param) +{ + int response; + + if (!acpi_evalf(hkey_handle, &response, method, "dd", param)) { + acpi_handle_err(hkey_handle, "%s: evaluate failed", method); + return AE_ERROR; + } + if (response & METHOD_ERR) { + acpi_handle_err(hkey_handle, + "%s evaluated but flagged as error", method); + return AE_ERROR; + } + *ret = response; + return AE_OK; +} + +static int tpacpi_battery_get(int what, int battery, int *ret) +{ + switch (what) { + case THRESHOLD_START: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, ret, battery)) + return -ENODEV; + + /* The value is in the low 8 bits of the response */ + *ret = *ret & 0xFF; + return 0; + case THRESHOLD_STOP: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, ret, battery)) + return -ENODEV; + /* Value is in lower 8 bits */ + *ret = *ret & 0xFF; + /* + * On the stop value, if we return 0 that + * does not make any sense. 0 means Default, which + * means that charging stops at 100%, so we return + * that. + */ + if (*ret == 0) + *ret = 100; + return 0; + case FORCE_DISCHARGE: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, ret, battery)) + return -ENODEV; + /* The force discharge status is in bit 0 */ + *ret = *ret & 0x01; + return 0; + case INHIBIT_CHARGE: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, ret, battery)) + return -ENODEV; + /* The inhibit charge status is in bit 0 */ + *ret = *ret & 0x01; + return 0; + default: + pr_crit("wrong parameter: %d", what); + return -EINVAL; + } +} + +static int tpacpi_battery_set(int what, int battery, int value) +{ + int param, ret; + /* The first 8 bits are the value of the threshold */ + param = value; + /* The battery ID is in bits 8-9, 2 bits */ + param |= battery << 8; + + switch (what) { + case THRESHOLD_START: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_START, &ret, param)) { + pr_err("failed to set charge threshold on battery %d", + battery); + return -ENODEV; + } + return 0; + case THRESHOLD_STOP: + if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_STOP, &ret, param)) { + pr_err("failed to set stop threshold: %d", battery); + return -ENODEV; + } + return 0; + case FORCE_DISCHARGE: + /* Force discharge is in bit 0, + * break on AC attach is in bit 1 (won't work on some ThinkPads), + * battery ID is in bits 8-9, 2 bits. + */ + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_DISCHARGE, &ret, param))) { + pr_err("failed to set force discharge on %d", battery); + return -ENODEV; + } + return 0; + case INHIBIT_CHARGE: + /* When setting inhibit charge, we set a default value of + * always breaking on AC detach and the effective time is set to + * be permanent. + * The battery ID is in bits 4-5, 2 bits, + * the effective time is in bits 8-23, 2 bytes. + * A time of FFFF indicates forever. + */ + param = value; + param |= battery << 4; + param |= 0xFFFF << 8; + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_INHIBIT, &ret, param))) { + pr_err("failed to set inhibit charge on %d", battery); + return -ENODEV; + } + return 0; + default: + pr_crit("wrong parameter: %d", what); + return -EINVAL; + } +} + +static int tpacpi_battery_set_validate(int what, int battery, int value) +{ + int ret, v; + + ret = tpacpi_battery_set(what, battery, value); + if (ret < 0) + return ret; + + ret = tpacpi_battery_get(what, battery, &v); + if (ret < 0) + return ret; + + if (v == value) + return 0; + + msleep(500); + + ret = tpacpi_battery_get(what, battery, &v); + if (ret < 0) + return ret; + + if (v == value) + return 0; + + return -EIO; +} + +static int tpacpi_battery_probe(int battery) +{ + int ret = 0; + + memset(&battery_info.batteries[battery], 0, + sizeof(battery_info.batteries[battery])); + + /* + * 1) Get the current start threshold + * 2) Check for support + * 3) Get the current stop threshold + * 4) Check for support + * 5) Get the current force discharge status + * 6) Check for support + * 7) Get the current inhibit charge status + * 8) Check for support + */ + if (acpi_has_method(hkey_handle, GET_START)) { + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, &ret, battery)) { + pr_err("Error probing battery %d\n", battery); + return -ENODEV; + } + /* Individual addressing is in bit 9 */ + if (ret & BIT(9)) + battery_info.individual_addressing = true; + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].start_support = 1; + else + return -ENODEV; + if (tpacpi_battery_get(THRESHOLD_START, battery, + &battery_info.batteries[battery].charge_start)) { + pr_err("Error probing battery %d\n", battery); + return -ENODEV; + } + } + if (acpi_has_method(hkey_handle, GET_STOP)) { + if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, &ret, battery)) { + pr_err("Error probing battery stop; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].stop_support = 1; + else + return -ENODEV; + if (tpacpi_battery_get(THRESHOLD_STOP, battery, + &battery_info.batteries[battery].charge_stop)) { + pr_err("Error probing battery stop: %d\n", battery); + return -ENODEV; + } + } + if (acpi_has_method(hkey_handle, GET_DISCHARGE)) { + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, &ret, battery))) { + pr_err("Error probing battery discharge; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 8 */ + if (ret & BIT(8)) + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE); + } + if (acpi_has_method(hkey_handle, GET_INHIBIT)) { + if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, &ret, battery))) { + pr_err("Error probing battery inhibit charge; %d\n", battery); + return -ENODEV; + } + /* Support is marked in bit 5 */ + if (ret & BIT(5)) + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE); + } + + battery_info.batteries[battery].charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO); + + pr_info("battery %d registered (start %d, stop %d, behaviours: 0x%x)\n", + battery, + battery_info.batteries[battery].charge_start, + battery_info.batteries[battery].charge_stop, + battery_info.batteries[battery].charge_behaviours); + + return 0; +} + +/* General helper functions */ + +static int tpacpi_battery_get_id(const char *battery_name) +{ + + if (strcmp(battery_name, "BAT0") == 0 || + tp_features.battery_force_primary) + return BAT_PRIMARY; + if (strcmp(battery_name, "BAT1") == 0) + return BAT_SECONDARY; + /* + * If for some reason the battery is not BAT0 nor is it + * BAT1, we will assume it's the default, first battery, + * AKA primary. + */ + pr_warn("unknown battery %s, assuming primary", battery_name); + return BAT_PRIMARY; +} + +/* sysfs interface */ + +static ssize_t tpacpi_battery_store(int what, + struct device *dev, + const char *buf, size_t count) +{ + struct power_supply *supply = to_power_supply(dev); + unsigned long value; + int battery, rval; + /* + * Some systems have support for more than + * one battery. If that is the case, + * tpacpi_battery_probe marked that addressing + * them individually is supported, so we do that + * based on the device struct. + * + * On systems that are not supported, we assume + * the primary as most of the ACPI calls fail + * with "Any Battery" as the parameter. + */ + if (battery_info.individual_addressing) + /* BAT_PRIMARY or BAT_SECONDARY */ + battery = tpacpi_battery_get_id(supply->desc->name); + else + battery = BAT_PRIMARY; + + rval = kstrtoul(buf, 10, &value); + if (rval) + return rval; + + switch (what) { + case THRESHOLD_START: + if (!battery_info.batteries[battery].start_support) + return -ENODEV; + /* valid values are [0, 99] */ + if (value > 99) + return -EINVAL; + if (value > battery_info.batteries[battery].charge_stop) + return -EINVAL; + if (tpacpi_battery_set(THRESHOLD_START, battery, value)) + return -ENODEV; + battery_info.batteries[battery].charge_start = value; + return count; + + case THRESHOLD_STOP: + if (!battery_info.batteries[battery].stop_support) + return -ENODEV; + /* valid values are [1, 100] */ + if (value < 1 || value > 100) + return -EINVAL; + if (value < battery_info.batteries[battery].charge_start) + return -EINVAL; + battery_info.batteries[battery].charge_stop = value; + /* + * When 100 is passed to stop, we need to flip + * it to 0 as that the EC understands that as + * "Default", which will charge to 100% + */ + if (value == 100) + value = 0; + if (tpacpi_battery_set(THRESHOLD_STOP, battery, value)) + return -EINVAL; + return count; + default: + pr_crit("Wrong parameter: %d", what); + return -EINVAL; + } + return count; +} + +static ssize_t tpacpi_battery_show(int what, + struct device *dev, + char *buf) +{ + struct power_supply *supply = to_power_supply(dev); + int ret, battery; + /* + * Some systems have support for more than + * one battery. If that is the case, + * tpacpi_battery_probe marked that addressing + * them individually is supported, so we; + * based on the device struct. + * + * On systems that are not supported, we assume + * the primary as most of the ACPI calls fail + * with "Any Battery" as the parameter. + */ + if (battery_info.individual_addressing) + /* BAT_PRIMARY or BAT_SECONDARY */ + battery = tpacpi_battery_get_id(supply->desc->name); + else + battery = BAT_PRIMARY; + if (tpacpi_battery_get(what, battery, &ret)) + return -ENODEV; + return sysfs_emit(buf, "%d\n", ret); +} + +static ssize_t charge_control_start_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_battery_show(THRESHOLD_START, device, buf); +} + +static ssize_t charge_control_end_threshold_show(struct device *device, + struct device_attribute *attr, + char *buf) +{ + return tpacpi_battery_show(THRESHOLD_STOP, device, buf); +} + +static ssize_t charge_behaviour_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + enum power_supply_charge_behaviour active = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + struct power_supply *supply = to_power_supply(dev); + unsigned int available; + int ret, battery; + + battery = tpacpi_battery_get_id(supply->desc->name); + available = battery_info.batteries[battery].charge_behaviours; + + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) { + if (tpacpi_battery_get(FORCE_DISCHARGE, battery, &ret)) + return -ENODEV; + if (ret) { + active = POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE; + goto out; + } + } + + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) { + if (tpacpi_battery_get(INHIBIT_CHARGE, battery, &ret)) + return -ENODEV; + if (ret) { + active = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + goto out; + } + } + +out: + return power_supply_charge_behaviour_show(dev, available, active, buf); +} + +static ssize_t charge_control_start_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_battery_store(THRESHOLD_START, dev, buf, count); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + return tpacpi_battery_store(THRESHOLD_STOP, dev, buf, count); +} + +static ssize_t charge_behaviour_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct power_supply *supply = to_power_supply(dev); + int selected, battery, ret = 0; + unsigned int available; + + battery = tpacpi_battery_get_id(supply->desc->name); + available = battery_info.batteries[battery].charge_behaviours; + selected = power_supply_charge_behaviour_parse(available, buf); + + if (selected < 0) + return selected; + + switch (selected) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) + ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) + ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0)); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) + ret = tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0); + ret = min(ret, tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 1)); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: + if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) + ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); + ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 1)); + if (ret < 0) + return ret; + break; + default: + dev_err(dev, "Unexpected charge behaviour: %d\n", selected); + return -EINVAL; + } + + return count; +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); +static DEVICE_ATTR_RW(charge_control_end_threshold); +static DEVICE_ATTR_RW(charge_behaviour); +static struct device_attribute dev_attr_charge_start_threshold = __ATTR( + charge_start_threshold, + 0644, + charge_control_start_threshold_show, + charge_control_start_threshold_store +); +static struct device_attribute dev_attr_charge_stop_threshold = __ATTR( + charge_stop_threshold, + 0644, + charge_control_end_threshold_show, + charge_control_end_threshold_store +); + +static struct attribute *tpacpi_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + &dev_attr_charge_start_threshold.attr, + &dev_attr_charge_stop_threshold.attr, + &dev_attr_charge_behaviour.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(tpacpi_battery); + +/* ACPI battery hooking */ + +static int tpacpi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + int batteryid = tpacpi_battery_get_id(battery->desc->name); + + if (tpacpi_battery_probe(batteryid)) + return -ENODEV; + if (device_add_groups(&battery->dev, tpacpi_battery_groups)) + return -ENODEV; + return 0; +} + +static int tpacpi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + device_remove_groups(&battery->dev, tpacpi_battery_groups); + return 0; +} + +static struct acpi_battery_hook battery_hook = { + .add_battery = tpacpi_battery_add, + .remove_battery = tpacpi_battery_remove, + .name = "ThinkPad Battery Extension", +}; + +/* Subdriver init/exit */ + +static const struct tpacpi_quirk battery_quirk_table[] __initconst = { + /* + * Individual addressing is broken on models that expose the + * primary battery as BAT1. + */ + TPACPI_Q_LNV('G', '8', true), /* ThinkPad X131e */ + TPACPI_Q_LNV('8', 'F', true), /* Thinkpad X120e */ + TPACPI_Q_LNV('J', '7', true), /* B5400 */ + TPACPI_Q_LNV('J', 'I', true), /* Thinkpad 11e */ + TPACPI_Q_LNV3('R', '0', 'B', true), /* Thinkpad 11e gen 3 */ + TPACPI_Q_LNV3('R', '0', 'C', true), /* Thinkpad 13 */ + TPACPI_Q_LNV3('R', '0', 'J', true), /* Thinkpad 13 gen 2 */ + TPACPI_Q_LNV3('R', '0', 'K', true), /* Thinkpad 11e gen 4 celeron BIOS */ +}; + +static int __init tpacpi_battery_init(struct ibm_init_struct *ibm) +{ + memset(&battery_info, 0, sizeof(battery_info)); + + tp_features.battery_force_primary = tpacpi_check_quirks( + battery_quirk_table, + ARRAY_SIZE(battery_quirk_table)); + + battery_hook_register(&battery_hook); + return 0; +} + +static void tpacpi_battery_exit(void) +{ + battery_hook_unregister(&battery_hook); +} + +static struct ibm_struct battery_driver_data = { + .name = "battery", + .exit = tpacpi_battery_exit, +}; + +/************************************************************************* + * LCD Shadow subdriver, for the Lenovo PrivacyGuard feature + */ + +static struct drm_privacy_screen *lcdshadow_dev; +static acpi_handle lcdshadow_get_handle; +static acpi_handle lcdshadow_set_handle; + +static int lcdshadow_set_sw_state(struct drm_privacy_screen *priv, + enum drm_privacy_screen_status state) +{ + int output; + + if (WARN_ON(!mutex_is_locked(&priv->lock))) + return -EIO; + + if (!acpi_evalf(lcdshadow_set_handle, &output, NULL, "dd", (int)state)) + return -EIO; + + priv->hw_state = priv->sw_state = state; + return 0; +} + +static void lcdshadow_get_hw_state(struct drm_privacy_screen *priv) +{ + int output; + + if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) + return; + + priv->hw_state = priv->sw_state = output & 0x1; +} + +static const struct drm_privacy_screen_ops lcdshadow_ops = { + .set_sw_state = lcdshadow_set_sw_state, + .get_hw_state = lcdshadow_get_hw_state, +}; + +static int tpacpi_lcdshadow_init(struct ibm_init_struct *iibm) +{ + acpi_status status1, status2; + int output; + + status1 = acpi_get_handle(hkey_handle, "GSSS", &lcdshadow_get_handle); + status2 = acpi_get_handle(hkey_handle, "SSSS", &lcdshadow_set_handle); + if (ACPI_FAILURE(status1) || ACPI_FAILURE(status2)) + return 0; + + if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) + return -EIO; + + if (!(output & 0x10000)) + return 0; + + lcdshadow_dev = drm_privacy_screen_register(&tpacpi_pdev->dev, + &lcdshadow_ops, NULL); + if (IS_ERR(lcdshadow_dev)) + return PTR_ERR(lcdshadow_dev); + + return 0; +} + +static void lcdshadow_exit(void) +{ + drm_privacy_screen_unregister(lcdshadow_dev); +} + +static void lcdshadow_resume(void) +{ + if (!lcdshadow_dev) + return; + + mutex_lock(&lcdshadow_dev->lock); + lcdshadow_set_sw_state(lcdshadow_dev, lcdshadow_dev->sw_state); + mutex_unlock(&lcdshadow_dev->lock); +} + +static int lcdshadow_read(struct seq_file *m) +{ + if (!lcdshadow_dev) { + seq_puts(m, "status:\t\tnot supported\n"); + } else { + seq_printf(m, "status:\t\t%d\n", lcdshadow_dev->hw_state); + seq_puts(m, "commands:\t0, 1\n"); + } + + return 0; +} + +static int lcdshadow_write(char *buf) +{ + char *cmd; + int res, state = -EINVAL; + + if (!lcdshadow_dev) + return -ENODEV; + + while ((cmd = strsep(&buf, ","))) { + res = kstrtoint(cmd, 10, &state); + if (res < 0) + return res; + } + + if (state >= 2 || state < 0) + return -EINVAL; + + mutex_lock(&lcdshadow_dev->lock); + res = lcdshadow_set_sw_state(lcdshadow_dev, state); + mutex_unlock(&lcdshadow_dev->lock); + + drm_privacy_screen_call_notifier_chain(lcdshadow_dev); + + return res; +} + +static struct ibm_struct lcdshadow_driver_data = { + .name = "lcdshadow", + .exit = lcdshadow_exit, + .resume = lcdshadow_resume, + .read = lcdshadow_read, + .write = lcdshadow_write, +}; + +/************************************************************************* + * Thinkpad sensor interfaces + */ + +#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ +#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ +#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ +#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ + +#define DYTC_CMD_GET 2 /* To get current IC function and mode */ +#define DYTC_GET_LAPMODE_BIT 17 /* Set when in lapmode */ + +#define PALMSENSOR_PRESENT_BIT 0 /* Determine if psensor present */ +#define PALMSENSOR_ON_BIT 1 /* psensor status */ + +static bool has_palmsensor; +static bool has_lapsensor; +static bool palm_state; +static bool lap_state; +static int dytc_version; + +static int dytc_command(int command, int *output) +{ + acpi_handle dytc_handle; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DYTC", &dytc_handle))) { + /* Platform doesn't support DYTC */ + return -ENODEV; + } + if (!acpi_evalf(dytc_handle, output, NULL, "dd", command)) + return -EIO; + return 0; +} + +static int lapsensor_get(bool *present, bool *state) +{ + int output, err; + + *present = false; + err = dytc_command(DYTC_CMD_GET, &output); + if (err) + return err; + + *present = true; /*If we get his far, we have lapmode support*/ + *state = output & BIT(DYTC_GET_LAPMODE_BIT) ? true : false; + return 0; +} + +static int palmsensor_get(bool *present, bool *state) +{ + acpi_handle psensor_handle; + int output; + + *present = false; + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GPSS", &psensor_handle))) + return -ENODEV; + if (!acpi_evalf(psensor_handle, &output, NULL, "d")) + return -EIO; + + *present = output & BIT(PALMSENSOR_PRESENT_BIT) ? true : false; + *state = output & BIT(PALMSENSOR_ON_BIT) ? true : false; + return 0; +} + +static void lapsensor_refresh(void) +{ + bool state; + int err; + + if (has_lapsensor) { + err = lapsensor_get(&has_lapsensor, &state); + if (err) + return; + if (lap_state != state) { + lap_state = state; + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "dytc_lapmode"); + } + } +} + +static void palmsensor_refresh(void) +{ + bool state; + int err; + + if (has_palmsensor) { + err = palmsensor_get(&has_palmsensor, &state); + if (err) + return; + if (palm_state != state) { + palm_state = state; + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "palmsensor"); + } + } +} + +static ssize_t dytc_lapmode_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + if (has_lapsensor) + return sysfs_emit(buf, "%d\n", lap_state); + return sysfs_emit(buf, "\n"); +} +static DEVICE_ATTR_RO(dytc_lapmode); + +static ssize_t palmsensor_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + if (has_palmsensor) + return sysfs_emit(buf, "%d\n", palm_state); + return sysfs_emit(buf, "\n"); +} +static DEVICE_ATTR_RO(palmsensor); + +static struct attribute *proxsensor_attributes[] = { + &dev_attr_dytc_lapmode.attr, + &dev_attr_palmsensor.attr, + NULL +}; + +static umode_t proxsensor_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &dev_attr_dytc_lapmode.attr) { + /* + * Platforms before DYTC version 5 claim to have a lap sensor, + * but it doesn't work, so we ignore them. + */ + if (!has_lapsensor || dytc_version < 5) + return 0; + } else if (attr == &dev_attr_palmsensor.attr) { + if (!has_palmsensor) + return 0; + } + + return attr->mode; +} + +static const struct attribute_group proxsensor_attr_group = { + .is_visible = proxsensor_attr_is_visible, + .attrs = proxsensor_attributes, +}; + +static int tpacpi_proxsensor_init(struct ibm_init_struct *iibm) +{ + int palm_err, lap_err; + + palm_err = palmsensor_get(&has_palmsensor, &palm_state); + lap_err = lapsensor_get(&has_lapsensor, &lap_state); + /* If support isn't available for both devices return -ENODEV */ + if ((palm_err == -ENODEV) && (lap_err == -ENODEV)) + return -ENODEV; + /* Otherwise, if there was an error return it */ + if (palm_err && (palm_err != -ENODEV)) + return palm_err; + if (lap_err && (lap_err != -ENODEV)) + return lap_err; + + return 0; +} + +static struct ibm_struct proxsensor_driver_data = { + .name = "proximity-sensor", +}; + +/************************************************************************* + * DYTC Platform Profile interface + */ + +#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ +#define DYTC_CMD_MMC_GET 8 /* To get current MMC function and mode */ +#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ + +#define DYTC_CMD_FUNC_CAP 3 /* To get DYTC capabilities */ +#define DYTC_FC_MMC 27 /* MMC Mode supported */ +#define DYTC_FC_PSC 29 /* PSC Mode supported */ +#define DYTC_FC_AMT 31 /* AMT mode supported */ + +#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ +#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ + +#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ +#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ +#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ + +#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ +#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ +#define DYTC_FUNCTION_MMC 11 /* Function = 11, MMC mode */ +#define DYTC_FUNCTION_PSC 13 /* Function = 13, PSC mode */ +#define DYTC_FUNCTION_AMT 15 /* Function = 15, AMT mode */ + +#define DYTC_MODE_AMT_ENABLE 0x1 /* Enable AMT (in balanced mode) */ +#define DYTC_MODE_AMT_DISABLE 0xF /* Disable AMT (in other modes) */ + +#define DYTC_MODE_MMC_PERFORM 2 /* High power mode aka performance */ +#define DYTC_MODE_MMC_LOWPOWER 3 /* Low power mode */ +#define DYTC_MODE_MMC_BALANCE 0xF /* Default mode aka balanced */ +#define DYTC_MODE_MMC_DEFAULT 0 /* Default mode from MMC_GET, aka balanced */ + +#define DYTC_MODE_PSC_LOWPOWER 3 /* Low power mode */ +#define DYTC_MODE_PSC_BALANCE 5 /* Default mode aka balanced */ +#define DYTC_MODE_PSC_PERFORM 7 /* High power mode aka performance */ + +#define DYTC_MODE_PSCV9_LOWPOWER 1 /* Low power mode */ +#define DYTC_MODE_PSCV9_BALANCE 3 /* Default mode aka balanced */ +#define DYTC_MODE_PSCV9_PERFORM 4 /* High power mode aka performance */ + +#define DYTC_ERR_MASK 0xF /* Bits 0-3 in cmd result are the error result */ +#define DYTC_ERR_SUCCESS 1 /* CMD completed successful */ + +#define DYTC_SET_COMMAND(function, mode, on) \ + (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ + (mode) << DYTC_SET_MODE_BIT | \ + (on) << DYTC_SET_VALID_BIT) + +#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 0) +#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 1) +static int dytc_control_amt(bool enable); +static bool dytc_amt_active; + +static enum platform_profile_option dytc_current_profile; +static atomic_t dytc_ignore_event = ATOMIC_INIT(0); +static DEFINE_MUTEX(dytc_mutex); +static int dytc_capabilities; +static bool dytc_mmc_get_available; +static int profile_force; + +static int platform_psc_profile_lowpower = DYTC_MODE_PSC_LOWPOWER; +static int platform_psc_profile_balanced = DYTC_MODE_PSC_BALANCE; +static int platform_psc_profile_performance = DYTC_MODE_PSC_PERFORM; + +static int convert_dytc_to_profile(int funcmode, int dytcmode, + enum platform_profile_option *profile) +{ + switch (funcmode) { + case DYTC_FUNCTION_MMC: + switch (dytcmode) { + case DYTC_MODE_MMC_LOWPOWER: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + case DYTC_MODE_MMC_DEFAULT: + case DYTC_MODE_MMC_BALANCE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case DYTC_MODE_MMC_PERFORM: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + default: /* Unknown mode */ + return -EINVAL; + } + return 0; + case DYTC_FUNCTION_PSC: + if (dytcmode == platform_psc_profile_lowpower) + *profile = PLATFORM_PROFILE_LOW_POWER; + else if (dytcmode == platform_psc_profile_balanced) + *profile = PLATFORM_PROFILE_BALANCED; + else if (dytcmode == platform_psc_profile_performance) + *profile = PLATFORM_PROFILE_PERFORMANCE; + else + return -EINVAL; + + return 0; + case DYTC_FUNCTION_AMT: + /* For now return balanced. It's the closest we have to 'auto' */ + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + default: + /* Unknown function */ + pr_debug("unknown function 0x%x\n", funcmode); + return -EOPNOTSUPP; + } + return 0; +} + +static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) +{ + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_LOWPOWER; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_lowpower; + break; + case PLATFORM_PROFILE_BALANCED: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_BALANCE; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_balanced; + break; + case PLATFORM_PROFILE_PERFORMANCE: + if (dytc_capabilities & BIT(DYTC_FC_MMC)) + *perfmode = DYTC_MODE_MMC_PERFORM; + else if (dytc_capabilities & BIT(DYTC_FC_PSC)) + *perfmode = platform_psc_profile_performance; + break; + default: /* Unknown profile */ + return -EOPNOTSUPP; + } + return 0; +} + +/* + * dytc_profile_get: Function to register with platform_profile + * handler. Returns current platform profile. + */ +static int dytc_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + *profile = dytc_current_profile; + return 0; +} + +static int dytc_control_amt(bool enable) +{ + int dummy; + int err; + int cmd; + + if (!(dytc_capabilities & BIT(DYTC_FC_AMT))) { + pr_warn("Attempting to toggle AMT on a system that doesn't advertise support\n"); + return -ENODEV; + } + + if (enable) + cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_ENABLE, enable); + else + cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_DISABLE, enable); + + pr_debug("%sabling AMT (cmd 0x%x)", enable ? "en":"dis", cmd); + err = dytc_command(cmd, &dummy); + if (err) + return err; + dytc_amt_active = enable; + return 0; +} + +/* + * Helper function - check if we are in CQL mode and if we are + * - disable CQL, + * - run the command + * - enable CQL + * If not in CQL mode, just run the command + */ +static int dytc_cql_command(int command, int *output) +{ + int err, cmd_err, dummy; + int cur_funcmode; + + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_command(DYTC_CMD_GET, output); + if (err) + return err; + + cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; + /* Check if we're OK to return immediately */ + if ((command == DYTC_CMD_GET) && (cur_funcmode != DYTC_FUNCTION_CQL)) + return 0; + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + atomic_inc(&dytc_ignore_event); + err = dytc_command(DYTC_DISABLE_CQL, &dummy); + if (err) + return err; + } + + cmd_err = dytc_command(command, output); + /* Check return condition after we've restored CQL state */ + + if (cur_funcmode == DYTC_FUNCTION_CQL) { + err = dytc_command(DYTC_ENABLE_CQL, &dummy); + if (err) + return err; + } + return cmd_err; +} + +/* + * dytc_profile_set: Function to register with platform_profile + * handler. Sets current platform profile. + */ +static int dytc_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + int perfmode; + int output; + int err; + + err = mutex_lock_interruptible(&dytc_mutex); + if (err) + return err; + + err = convert_profile_to_dytc(profile, &perfmode); + if (err) + goto unlock; + + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { + if (profile == PLATFORM_PROFILE_BALANCED) { + /* + * To get back to balanced mode we need to issue a reset command. + * Note we still need to disable CQL mode before hand and re-enable + * it afterwards, otherwise dytc_lapmode gets reset to 0 and stays + * stuck at 0 for aprox. 30 minutes. + */ + err = dytc_cql_command(DYTC_CMD_RESET, &output); + if (err) + goto unlock; + } else { + /* Determine if we are in CQL mode. This alters the commands we do */ + err = dytc_cql_command(DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), + &output); + if (err) + goto unlock; + } + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { + err = dytc_command(DYTC_SET_COMMAND(DYTC_FUNCTION_PSC, perfmode, 1), &output); + if (err) + goto unlock; + + /* system supports AMT, activate it when on balanced */ + if (dytc_capabilities & BIT(DYTC_FC_AMT)) + dytc_control_amt(profile == PLATFORM_PROFILE_BALANCED); + } + /* Success - update current profile */ + dytc_current_profile = profile; +unlock: + mutex_unlock(&dytc_mutex); + return err; +} + +static int dytc_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + + return 0; +} + +static const struct platform_profile_ops dytc_profile_ops = { + .probe = dytc_profile_probe, + .profile_get = dytc_profile_get, + .profile_set = dytc_profile_set, +}; + +static void dytc_profile_refresh(void) +{ + enum platform_profile_option profile; + int output = 0, err = 0; + int perfmode, funcmode = 0; + + mutex_lock(&dytc_mutex); + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { + if (dytc_mmc_get_available) + err = dytc_command(DYTC_CMD_MMC_GET, &output); + else + err = dytc_cql_command(DYTC_CMD_GET, &output); + funcmode = DYTC_FUNCTION_MMC; + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { + err = dytc_command(DYTC_CMD_GET, &output); + /* Check if we are PSC mode, or have AMT enabled */ + funcmode = (output >> DYTC_GET_FUNCTION_BIT) & 0xF; + } else { /* Unknown profile mode */ + err = -ENODEV; + } + mutex_unlock(&dytc_mutex); + if (err) + return; + + perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; + err = convert_dytc_to_profile(funcmode, perfmode, &profile); + if (!err && profile != dytc_current_profile) { + dytc_current_profile = profile; + platform_profile_notify(tpacpi_pprof); + } +} + +static int tpacpi_dytc_profile_init(struct ibm_init_struct *iibm) +{ + int err, output; + + err = dytc_command(DYTC_CMD_QUERY, &output); + if (err) + return err; + + if (output & BIT(DYTC_QUERY_ENABLE_BIT)) + dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; + + dbg_printk(TPACPI_DBG_INIT, "DYTC version %d\n", dytc_version); + /* Check DYTC is enabled and supports mode setting */ + if (dytc_version < 5) + return -ENODEV; + + /* Check what capabilities are supported */ + err = dytc_command(DYTC_CMD_FUNC_CAP, &dytc_capabilities); + if (err) + return err; + + /* Check if user wants to override the profile selection */ + if (profile_force) { + switch (profile_force) { + case -1: + dytc_capabilities = 0; + break; + case 1: + dytc_capabilities = BIT(DYTC_FC_MMC); + break; + case 2: + dytc_capabilities = BIT(DYTC_FC_PSC); + break; + } + pr_debug("Profile selection forced: 0x%x\n", dytc_capabilities); + } + if (dytc_capabilities & BIT(DYTC_FC_MMC)) { /* MMC MODE */ + pr_debug("MMC is supported\n"); + /* + * Check if MMC_GET functionality available + * Version > 6 and return success from MMC_GET command + */ + dytc_mmc_get_available = false; + if (dytc_version >= 6) { + err = dytc_command(DYTC_CMD_MMC_GET, &output); + if (!err && ((output & DYTC_ERR_MASK) == DYTC_ERR_SUCCESS)) + dytc_mmc_get_available = true; + } + } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { /* PSC MODE */ + pr_debug("PSC is supported\n"); + if (dytc_version >= 9) { /* update profiles for DYTC 9 and up */ + platform_psc_profile_lowpower = DYTC_MODE_PSCV9_LOWPOWER; + platform_psc_profile_balanced = DYTC_MODE_PSCV9_BALANCE; + platform_psc_profile_performance = DYTC_MODE_PSCV9_PERFORM; + } + } else { + dbg_printk(TPACPI_DBG_INIT, "No DYTC support available\n"); + return -ENODEV; + } + + dbg_printk(TPACPI_DBG_INIT, + "DYTC version %d: thermal mode available\n", dytc_version); + + /* Create platform_profile structure and register */ + tpacpi_pprof = platform_profile_register(&tpacpi_pdev->dev, "thinkpad-acpi-profile", + NULL, &dytc_profile_ops); + /* + * If for some reason platform_profiles aren't enabled + * don't quit terminally. + */ + if (IS_ERR(tpacpi_pprof)) + return -ENODEV; + + /* Ensure initial values are correct */ + dytc_profile_refresh(); + + /* Workaround for https://bugzilla.kernel.org/show_bug.cgi?id=216347 */ + if (dytc_capabilities & BIT(DYTC_FC_PSC)) + dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); + + return 0; +} + +static void dytc_profile_exit(void) +{ + if (!IS_ERR_OR_NULL(tpacpi_pprof)) + platform_profile_remove(tpacpi_pprof); +} + +static struct ibm_struct dytc_profile_driver_data = { + .name = "dytc-profile", + .exit = dytc_profile_exit, +}; + +/************************************************************************* + * Keyboard language interface + */ + +struct keyboard_lang_data { + const char *lang_str; + int lang_code; +}; + +static const struct keyboard_lang_data keyboard_lang_data[] = { + {"be", 0x080c}, + {"cz", 0x0405}, + {"da", 0x0406}, + {"de", 0x0c07}, + {"en", 0x0000}, + {"es", 0x2c0a}, + {"et", 0x0425}, + {"fr", 0x040c}, + {"fr-ch", 0x100c}, + {"hu", 0x040e}, + {"it", 0x0410}, + {"jp", 0x0411}, + {"nl", 0x0413}, + {"nn", 0x0414}, + {"pl", 0x0415}, + {"pt", 0x0816}, + {"sl", 0x041b}, + {"sv", 0x081d}, + {"tr", 0x041f}, +}; + +static int set_keyboard_lang_command(int command) +{ + acpi_handle sskl_handle; + int output; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "SSKL", &sskl_handle))) { + /* Platform doesn't support SSKL */ + return -ENODEV; + } + + if (!acpi_evalf(sskl_handle, &output, NULL, "dd", command)) + return -EIO; + + return 0; +} + +static int get_keyboard_lang(int *output) +{ + acpi_handle gskl_handle; + int kbd_lang; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GSKL", &gskl_handle))) { + /* Platform doesn't support GSKL */ + return -ENODEV; + } + + if (!acpi_evalf(gskl_handle, &kbd_lang, NULL, "dd", 0x02000000)) + return -EIO; + + /* + * METHOD_ERR gets returned on devices where there are no special (e.g. '=', + * '(' and ')') keys which use layout dependent key-press emulation. + */ + if (kbd_lang & METHOD_ERR) + return -ENODEV; + + *output = kbd_lang; + + return 0; +} + +/* sysfs keyboard language entry */ +static ssize_t keyboard_lang_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int output, err, i, len = 0; + + err = get_keyboard_lang(&output); + if (err) + return err; + + for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { + if (i) + len += sysfs_emit_at(buf, len, "%s", " "); + + if (output == keyboard_lang_data[i].lang_code) { + len += sysfs_emit_at(buf, len, "[%s]", keyboard_lang_data[i].lang_str); + } else { + len += sysfs_emit_at(buf, len, "%s", keyboard_lang_data[i].lang_str); + } + } + len += sysfs_emit_at(buf, len, "\n"); + + return len; +} + +static ssize_t keyboard_lang_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int err, i; + bool lang_found = false; + int lang_code = 0; + + for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { + if (sysfs_streq(buf, keyboard_lang_data[i].lang_str)) { + lang_code = keyboard_lang_data[i].lang_code; + lang_found = true; + break; + } + } + + if (lang_found) { + lang_code = lang_code | 1 << 24; + + /* Set language code */ + err = set_keyboard_lang_command(lang_code); + if (err) + return err; + } else { + dev_err(&tpacpi_pdev->dev, "Unknown Keyboard language. Ignoring\n"); + return -EINVAL; + } + + tpacpi_disclose_usertask(attr->attr.name, + "keyboard language is set to %s\n", buf); + + sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "keyboard_lang"); + + return count; +} +static DEVICE_ATTR_RW(keyboard_lang); + +static struct attribute *kbdlang_attributes[] = { + &dev_attr_keyboard_lang.attr, + NULL +}; + +static umode_t kbdlang_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return tp_features.kbd_lang ? attr->mode : 0; +} + +static const struct attribute_group kbdlang_attr_group = { + .is_visible = kbdlang_attr_is_visible, + .attrs = kbdlang_attributes, +}; + +static int tpacpi_kbdlang_init(struct ibm_init_struct *iibm) +{ + int err, output; + + err = get_keyboard_lang(&output); + tp_features.kbd_lang = !err; + return err; +} + +static struct ibm_struct kbdlang_driver_data = { + .name = "kbdlang", +}; + +/************************************************************************* + * DPRC(Dynamic Power Reduction Control) subdriver, for the Lenovo WWAN + * and WLAN feature. + */ +#define DPRC_GET_WWAN_ANTENNA_TYPE 0x40000 +#define DPRC_WWAN_ANTENNA_TYPE_A_BIT BIT(4) +#define DPRC_WWAN_ANTENNA_TYPE_B_BIT BIT(8) +static bool has_antennatype; +static int wwan_antennatype; + +static int dprc_command(int command, int *output) +{ + acpi_handle dprc_handle; + + if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DPRC", &dprc_handle))) { + /* Platform doesn't support DPRC */ + return -ENODEV; + } + + if (!acpi_evalf(dprc_handle, output, NULL, "dd", command)) + return -EIO; + + /* + * METHOD_ERR gets returned on devices where few commands are not supported + * for example command to get WWAN Antenna type command is not supported on + * some devices. + */ + if (*output & METHOD_ERR) + return -ENODEV; + + return 0; +} + +static int get_wwan_antenna(int *wwan_antennatype) +{ + int output, err; + + /* Get current Antenna type */ + err = dprc_command(DPRC_GET_WWAN_ANTENNA_TYPE, &output); + if (err) + return err; + + if (output & DPRC_WWAN_ANTENNA_TYPE_A_BIT) + *wwan_antennatype = 1; + else if (output & DPRC_WWAN_ANTENNA_TYPE_B_BIT) + *wwan_antennatype = 2; + else + return -ENODEV; + + return 0; +} + +/* sysfs wwan antenna type entry */ +static ssize_t wwan_antenna_type_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + switch (wwan_antennatype) { + case 1: + return sysfs_emit(buf, "type a\n"); + case 2: + return sysfs_emit(buf, "type b\n"); + default: + return -ENODATA; + } +} +static DEVICE_ATTR_RO(wwan_antenna_type); + +static struct attribute *dprc_attributes[] = { + &dev_attr_wwan_antenna_type.attr, + NULL +}; + +static umode_t dprc_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return has_antennatype ? attr->mode : 0; +} + +static const struct attribute_group dprc_attr_group = { + .is_visible = dprc_attr_is_visible, + .attrs = dprc_attributes, +}; + +static int tpacpi_dprc_init(struct ibm_init_struct *iibm) +{ + int err; + + err = get_wwan_antenna(&wwan_antennatype); + if (err) + return err; + + has_antennatype = true; + return 0; +} + +static struct ibm_struct dprc_driver_data = { + .name = "dprc", +}; + +/* + * Auxmac + * + * This auxiliary mac address is enabled in the bios through the + * MAC Address Pass-through feature. In most cases, there are three + * possibilities: Internal Mac, Second Mac, and disabled. + * + */ + +#define AUXMAC_LEN 12 +#define AUXMAC_START 9 +#define AUXMAC_STRLEN 22 +#define AUXMAC_BEGIN_MARKER 8 +#define AUXMAC_END_MARKER 21 + +static char auxmac[AUXMAC_LEN + 1]; + +static int auxmac_init(struct ibm_init_struct *iibm) +{ + acpi_status status; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + + status = acpi_evaluate_object(NULL, "\\MACA", NULL, &buffer); + + if (ACPI_FAILURE(status)) + return -ENODEV; + + obj = buffer.pointer; + + if (obj->type != ACPI_TYPE_STRING || obj->string.length != AUXMAC_STRLEN) { + pr_info("Invalid buffer for MAC address pass-through.\n"); + goto auxmacinvalid; + } + + if (obj->string.pointer[AUXMAC_BEGIN_MARKER] != '#' || + obj->string.pointer[AUXMAC_END_MARKER] != '#') { + pr_info("Invalid header for MAC address pass-through.\n"); + goto auxmacinvalid; + } + + if (strncmp(obj->string.pointer + AUXMAC_START, "XXXXXXXXXXXX", AUXMAC_LEN) != 0) + strscpy(auxmac, obj->string.pointer + AUXMAC_START, sizeof(auxmac)); + else + strscpy(auxmac, "disabled", sizeof(auxmac)); + +free: + kfree(obj); + return 0; + +auxmacinvalid: + strscpy(auxmac, "unavailable", sizeof(auxmac)); + goto free; +} + +static struct ibm_struct auxmac_data = { + .name = "auxmac", +}; + +static DEVICE_STRING_ATTR_RO(auxmac, 0444, auxmac); + +static umode_t auxmac_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + return auxmac[0] == 0 ? 0 : attr->mode; +} + +static struct attribute *auxmac_attributes[] = { + &dev_attr_auxmac.attr.attr, + NULL +}; + +static const struct attribute_group auxmac_attr_group = { + .is_visible = auxmac_attr_is_visible, + .attrs = auxmac_attributes, +}; + +/* --------------------------------------------------------------------- */ + +static struct attribute *tpacpi_driver_attributes[] = { + &driver_attr_debug_level.attr, + &driver_attr_version.attr, + &driver_attr_interface_version.attr, +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + &driver_attr_wlsw_emulstate.attr, + &driver_attr_bluetooth_emulstate.attr, + &driver_attr_wwan_emulstate.attr, + &driver_attr_uwb_emulstate.attr, +#endif + NULL +}; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +static umode_t tpacpi_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + if (attr == &driver_attr_wlsw_emulstate.attr) { + if (!dbg_wlswemul) + return 0; + } else if (attr == &driver_attr_bluetooth_emulstate.attr) { + if (!dbg_bluetoothemul) + return 0; + } else if (attr == &driver_attr_wwan_emulstate.attr) { + if (!dbg_wwanemul) + return 0; + } else if (attr == &driver_attr_uwb_emulstate.attr) { + if (!dbg_uwbemul) + return 0; + } + + return attr->mode; +} +#endif + +static const struct attribute_group tpacpi_driver_attr_group = { +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES + .is_visible = tpacpi_attr_is_visible, +#endif + .attrs = tpacpi_driver_attributes, +}; + +static const struct attribute_group *tpacpi_driver_groups[] = { + &tpacpi_driver_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_groups[] = { + &adaptive_kbd_attr_group, + &hotkey_attr_group, + &bluetooth_attr_group, + &wan_attr_group, + &cmos_attr_group, + &proxsensor_attr_group, + &kbdlang_attr_group, + &dprc_attr_group, + &auxmac_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_hwmon_groups[] = { + &thermal_attr_group, + &temp_label_attr_group, + &fan_attr_group, + NULL, +}; + +static const struct attribute_group *tpacpi_hwmon_driver_groups[] = { + &fan_driver_attr_group, + NULL, +}; + +/**************************************************************************** + **************************************************************************** + * + * Platform drivers + * + **************************************************************************** + ****************************************************************************/ + +static struct platform_driver tpacpi_pdriver = { + .driver = { + .name = TPACPI_DRVR_NAME, + .pm = &tpacpi_pm, + .groups = tpacpi_driver_groups, + .dev_groups = tpacpi_groups, + }, + .shutdown = tpacpi_shutdown_handler, +}; + +static struct platform_driver tpacpi_hwmon_pdriver = { + .driver = { + .name = TPACPI_HWMON_DRVR_NAME, + .groups = tpacpi_hwmon_driver_groups, + }, +}; + +/**************************************************************************** + **************************************************************************** + * + * Infrastructure + * + **************************************************************************** + ****************************************************************************/ + +/* + * HKEY event callout for other subdrivers go here + * (yes, it is ugly, but it is quick, safe, and gets the job done + */ +static bool tpacpi_driver_event(const unsigned int hkey_event) +{ + int camera_shutter_state; + + switch (hkey_event) { + case TP_HKEY_EV_BRGHT_UP: + case TP_HKEY_EV_BRGHT_DOWN: + if (ibm_backlight_device) + tpacpi_brightness_notify_change(); + /* + * Key press events are suppressed by default hotkey_user_mask + * and should still be reported if explicitly requested. + */ + return false; + case TP_HKEY_EV_VOL_UP: + case TP_HKEY_EV_VOL_DOWN: + case TP_HKEY_EV_VOL_MUTE: + if (alsa_card) + volume_alsa_notify_change(); + + /* Key events are suppressed by default hotkey_user_mask */ + return false; + case TP_HKEY_EV_KBD_LIGHT: + if (tp_features.kbdlight) { + enum led_brightness brightness; + + mutex_lock(&kbdlight_mutex); + + /* + * Check the brightness actually changed, setting the brightness + * through kbdlight_set_level() also triggers this event. + */ + brightness = kbdlight_sysfs_get(NULL); + if (kbdlight_brightness != brightness) { + kbdlight_brightness = brightness; + led_classdev_notify_brightness_hw_changed( + &tpacpi_led_kbdlight.led_classdev, brightness); + } + + mutex_unlock(&kbdlight_mutex); + } + /* Key events are suppressed by default hotkey_user_mask */ + return false; + case TP_HKEY_EV_DFR_CHANGE_ROW: + adaptive_keyboard_change_row(); + return true; + case TP_HKEY_EV_DFR_S_QUICKVIEW_ROW: + adaptive_keyboard_s_quickview_row(); + return true; + case TP_HKEY_EV_THM_CSM_COMPLETED: + lapsensor_refresh(); + /* If we are already accessing DYTC then skip dytc update */ + if (!atomic_add_unless(&dytc_ignore_event, -1, 0)) + dytc_profile_refresh(); + + return true; + case TP_HKEY_EV_PRIVACYGUARD_TOGGLE: + if (lcdshadow_dev) { + enum drm_privacy_screen_status old_hw_state; + bool changed; + + mutex_lock(&lcdshadow_dev->lock); + old_hw_state = lcdshadow_dev->hw_state; + lcdshadow_get_hw_state(lcdshadow_dev); + changed = lcdshadow_dev->hw_state != old_hw_state; + mutex_unlock(&lcdshadow_dev->lock); + + if (changed) + drm_privacy_screen_call_notifier_chain(lcdshadow_dev); + } + return true; + case TP_HKEY_EV_AMT_TOGGLE: + /* If we're enabling AMT we need to force balanced mode */ + if (!dytc_amt_active) + /* This will also set AMT mode enabled */ + dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); + else + dytc_control_amt(!dytc_amt_active); + + return true; + case TP_HKEY_EV_CAMERASHUTTER_TOGGLE: + camera_shutter_state = get_camera_shutter(); + if (camera_shutter_state < 0) { + pr_err("Error retrieving camera shutter state after shutter event\n"); + return true; + } + mutex_lock(&tpacpi_inputdev_send_mutex); + + input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); + input_sync(tpacpi_inputdev); + + mutex_unlock(&tpacpi_inputdev_send_mutex); + return true; + case TP_HKEY_EV_DOUBLETAP_TOGGLE: + tp_features.trackpoint_doubletap = !tp_features.trackpoint_doubletap; + return true; + case TP_HKEY_EV_PROFILE_TOGGLE: + case TP_HKEY_EV_PROFILE_TOGGLE2: + platform_profile_cycle(); + return true; + } + + return false; +} + +/* --------------------------------------------------------------------- */ + +/* /proc support */ +static struct proc_dir_entry *proc_dir; + +/* + * Module and infrastructure proble, init and exit handling + */ + +static bool force_load; + +#ifdef CONFIG_THINKPAD_ACPI_DEBUG +static const char * __init str_supported(int is_supported) +{ + static char text_unsupported[] __initdata = "not supported"; + + return (is_supported) ? &text_unsupported[4] : &text_unsupported[0]; +} +#endif /* CONFIG_THINKPAD_ACPI_DEBUG */ + +static void ibm_exit(struct ibm_struct *ibm) +{ + dbg_printk(TPACPI_DBG_EXIT, "removing %s\n", ibm->name); + + list_del_init(&ibm->all_drivers); + + if (ibm->flags.acpi_notify_installed) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: acpi_remove_notify_handler\n", ibm->name); + BUG_ON(!ibm->acpi); + acpi_remove_notify_handler(*ibm->acpi->handle, + ibm->acpi->type, + dispatch_acpi_notify); + ibm->flags.acpi_notify_installed = 0; + } + + if (ibm->flags.proc_created) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: remove_proc_entry\n", ibm->name); + remove_proc_entry(ibm->name, proc_dir); + ibm->flags.proc_created = 0; + } + + if (ibm->flags.acpi_driver_registered) { + dbg_printk(TPACPI_DBG_EXIT, + "%s: acpi_bus_unregister_driver\n", ibm->name); + BUG_ON(!ibm->acpi); + acpi_bus_unregister_driver(ibm->acpi->driver); + kfree(ibm->acpi->driver); + ibm->acpi->driver = NULL; + ibm->flags.acpi_driver_registered = 0; + } + + if (ibm->flags.init_called && ibm->exit) { + ibm->exit(); + ibm->flags.init_called = 0; + } + + dbg_printk(TPACPI_DBG_INIT, "finished removing %s\n", ibm->name); +} + +static int __init ibm_init(struct ibm_init_struct *iibm) +{ + int ret; + struct ibm_struct *ibm = iibm->data; + struct proc_dir_entry *entry; + + BUG_ON(ibm == NULL); + + INIT_LIST_HEAD(&ibm->all_drivers); + + if (ibm->flags.experimental && !experimental) + return 0; + + dbg_printk(TPACPI_DBG_INIT, + "probing for %s\n", ibm->name); + + if (iibm->init) { + ret = iibm->init(iibm); + if (ret > 0 || ret == -ENODEV) + return 0; /* subdriver functionality not available */ + if (ret) + return ret; + + ibm->flags.init_called = 1; + } + + if (ibm->acpi) { + if (ibm->acpi->hid) { + ret = register_tpacpi_subdriver(ibm); + if (ret) + goto err_out; + } + + if (ibm->acpi->notify) { + ret = setup_acpi_notify(ibm); + if (ret == -ENODEV) { + pr_notice("disabling subdriver %s\n", + ibm->name); + ret = 0; + goto err_out; + } + if (ret < 0) + goto err_out; + } + } + + dbg_printk(TPACPI_DBG_INIT, + "%s installed\n", ibm->name); + + if (ibm->read) { + umode_t mode = iibm->base_procfs_mode; + + if (!mode) + mode = S_IRUGO; + if (ibm->write) + mode |= S_IWUSR; + entry = proc_create_data(ibm->name, mode, proc_dir, + &dispatch_proc_ops, ibm); + if (!entry) { + pr_err("unable to create proc entry %s\n", ibm->name); + ret = -ENODEV; + goto err_out; + } + ibm->flags.proc_created = 1; + } + + list_add_tail(&ibm->all_drivers, &tpacpi_all_drivers); + + return 0; + +err_out: + dbg_printk(TPACPI_DBG_INIT, + "%s: at error exit path with result %d\n", + ibm->name, ret); + + ibm_exit(ibm); + return (ret < 0) ? ret : 0; +} + +/* Probing */ + +static char __init tpacpi_parse_fw_id(const char * const s, + u32 *model, u16 *release) +{ + int i; + + if (!s || strlen(s) < 8) + goto invalid; + + for (i = 0; i < 8; i++) + if (!((s[i] >= '0' && s[i] <= '9') || + (s[i] >= 'A' && s[i] <= 'Z'))) + goto invalid; + + /* + * Most models: xxyTkkWW (#.##c) + * Ancient 570/600 and -SL lacks (#.##c) + */ + if (s[3] == 'T' || s[3] == 'N') { + *model = TPID(s[0], s[1]); + *release = TPVER(s[4], s[5]); + return s[2]; + + /* New models: xxxyTkkW (#.##c); T550 and some others */ + } else if (s[4] == 'T' || s[4] == 'N') { + *model = TPID3(s[0], s[1], s[2]); + *release = TPVER(s[5], s[6]); + return s[3]; + } + +invalid: + return '\0'; +} + +#define EC_FW_STRING_LEN 18 + +static void find_new_ec_fwstr(const struct dmi_header *dm, void *private) +{ + char *ec_fw_string = (char *) private; + const char *dmi_data = (const char *)dm; + /* + * ThinkPad Embedded Controller Program Table on newer models + * + * Offset | Name | Width | Description + * ---------------------------------------------------- + * 0x00 | Type | BYTE | 0x8C + * 0x01 | Length | BYTE | + * 0x02 | Handle | WORD | Varies + * 0x04 | Signature | BYTEx6 | ASCII for "LENOVO" + * 0x0A | OEM struct offset | BYTE | 0x0B + * 0x0B | OEM struct number | BYTE | 0x07, for this structure + * 0x0C | OEM struct revision | BYTE | 0x01, for this format + * 0x0D | ECP version ID | STR ID | + * 0x0E | ECP release date | STR ID | + */ + + /* Return if data structure not match */ + if (dm->type != 140 || dm->length < 0x0F || + memcmp(dmi_data + 4, "LENOVO", 6) != 0 || + dmi_data[0x0A] != 0x0B || dmi_data[0x0B] != 0x07 || + dmi_data[0x0C] != 0x01) + return; + + /* fwstr is the first 8byte string */ + BUILD_BUG_ON(EC_FW_STRING_LEN <= 8); + memcpy(ec_fw_string, dmi_data + 0x0F, 8); +} + +/* returns 0 - probe ok, or < 0 - probe error. + * Probe ok doesn't mean thinkpad found. + * On error, kfree() cleanup on tp->* is not performed, caller must do it */ +static int __must_check __init get_thinkpad_model_data( + struct thinkpad_id_data *tp) +{ + const struct dmi_device *dev = NULL; + char ec_fw_string[EC_FW_STRING_LEN] = {0}; + char const *s; + char t; + + if (!tp) + return -EINVAL; + + memset(tp, 0, sizeof(*tp)); + + if (dmi_name_in_vendors("IBM")) + tp->vendor = PCI_VENDOR_ID_IBM; + else if (dmi_name_in_vendors("LENOVO")) + tp->vendor = PCI_VENDOR_ID_LENOVO; + else if (dmi_name_in_vendors("NEC")) + tp->vendor = PCI_VENDOR_ID_LENOVO; + else + return 0; + + s = dmi_get_system_info(DMI_BIOS_VERSION); + tp->bios_version_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->bios_version_str) + return -ENOMEM; + + /* Really ancient ThinkPad 240X will fail this, which is fine */ + t = tpacpi_parse_fw_id(tp->bios_version_str, + &tp->bios_model, &tp->bios_release); + if (t != 'E' && t != 'C') + return 0; + + /* + * ThinkPad T23 or newer, A31 or newer, R50e or newer, + * X32 or newer, all Z series; Some models must have an + * up-to-date BIOS or they will not be detected. + * + * See https://thinkwiki.org/wiki/List_of_DMI_IDs + */ + while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) { + if (sscanf(dev->name, + "IBM ThinkPad Embedded Controller -[%17c", + ec_fw_string) == 1) { + ec_fw_string[sizeof(ec_fw_string) - 1] = 0; + ec_fw_string[strcspn(ec_fw_string, " ]")] = 0; + break; + } + } + + /* Newer ThinkPads have different EC program info table */ + if (!ec_fw_string[0]) + dmi_walk(find_new_ec_fwstr, &ec_fw_string); + + if (ec_fw_string[0]) { + tp->ec_version_str = kstrdup(ec_fw_string, GFP_KERNEL); + if (!tp->ec_version_str) + return -ENOMEM; + + t = tpacpi_parse_fw_id(ec_fw_string, + &tp->ec_model, &tp->ec_release); + if (t != 'H') { + pr_notice("ThinkPad firmware release %s doesn't match the known patterns\n", + ec_fw_string); + pr_notice("please report this to %s\n", TPACPI_MAIL); + } + } + + s = dmi_get_system_info(DMI_PRODUCT_VERSION); + if (s && !(strncasecmp(s, "ThinkPad", 8) && strncasecmp(s, "Lenovo", 6))) { + tp->model_str = kstrdup(s, GFP_KERNEL); + if (!tp->model_str) + return -ENOMEM; + } else { + s = dmi_get_system_info(DMI_BIOS_VENDOR); + if (s && !(strncasecmp(s, "Lenovo", 6))) { + tp->model_str = kstrdup(s, GFP_KERNEL); + if (!tp->model_str) + return -ENOMEM; + } + } + + s = dmi_get_system_info(DMI_PRODUCT_NAME); + tp->nummodel_str = kstrdup(s, GFP_KERNEL); + if (s && !tp->nummodel_str) + return -ENOMEM; + + return 0; +} + +static int __init probe_for_thinkpad(void) +{ + int is_thinkpad; + + if (acpi_disabled) + return -ENODEV; + + /* It would be dangerous to run the driver in this case */ + if (!tpacpi_is_ibm() && !tpacpi_is_lenovo()) + return -ENODEV; + + /* + * Non-ancient models have better DMI tagging, but very old models + * don't. tpacpi_is_fw_known() is a cheat to help in that case. + */ + is_thinkpad = (thinkpad_id.model_str != NULL) || + (thinkpad_id.ec_model != 0) || + tpacpi_is_fw_known(); + + /* The EC handler is required */ + tpacpi_acpi_handle_locate("ec", TPACPI_ACPI_EC_HID, &ec_handle); + if (!ec_handle) { + if (is_thinkpad) + pr_err("Not yet supported ThinkPad detected!\n"); + return -ENODEV; + } + + if (!is_thinkpad && !force_load) + return -ENODEV; + + return 0; +} + +static void __init thinkpad_acpi_init_banner(void) +{ + pr_info("%s v%s\n", TPACPI_DESC, TPACPI_VERSION); + pr_info("%s\n", TPACPI_URL); + + pr_info("ThinkPad BIOS %s, EC %s\n", + (thinkpad_id.bios_version_str) ? + thinkpad_id.bios_version_str : "unknown", + (thinkpad_id.ec_version_str) ? + thinkpad_id.ec_version_str : "unknown"); + + BUG_ON(!thinkpad_id.vendor); + + if (thinkpad_id.model_str) + pr_info("%s %s, model %s\n", + (thinkpad_id.vendor == PCI_VENDOR_ID_IBM) ? + "IBM" : ((thinkpad_id.vendor == + PCI_VENDOR_ID_LENOVO) ? + "Lenovo" : "Unknown vendor"), + thinkpad_id.model_str, + (thinkpad_id.nummodel_str) ? + thinkpad_id.nummodel_str : "unknown"); +} + +/* Module init, exit, parameters */ + +static struct ibm_init_struct ibms_init[] __initdata = { + { + .data = &thinkpad_acpi_driver_data, + }, + { + .init = hotkey_init, + .data = &hotkey_driver_data, + }, + { + .init = bluetooth_init, + .data = &bluetooth_driver_data, + }, + { + .init = wan_init, + .data = &wan_driver_data, + }, + { + .init = uwb_init, + .data = &uwb_driver_data, + }, +#ifdef CONFIG_THINKPAD_ACPI_VIDEO + { + .init = video_init, + .base_procfs_mode = S_IRUSR, + .data = &video_driver_data, + }, +#endif + { + .init = kbdlight_init, + .data = &kbdlight_driver_data, + }, + { + .init = light_init, + .data = &light_driver_data, + }, + { + .init = cmos_init, + .data = &cmos_driver_data, + }, + { + .init = led_init, + .data = &led_driver_data, + }, + { + .init = beep_init, + .data = &beep_driver_data, + }, + { + .init = thermal_init, + .data = &thermal_driver_data, + }, + { + .init = brightness_init, + .data = &brightness_driver_data, + }, + { + .init = volume_init, + .data = &volume_driver_data, + }, + { + .init = fan_init, + .data = &fan_driver_data, + }, + { + .init = mute_led_init, + .data = &mute_led_driver_data, + }, + { + .init = tpacpi_battery_init, + .data = &battery_driver_data, + }, + { + .init = tpacpi_lcdshadow_init, + .data = &lcdshadow_driver_data, + }, + { + .init = tpacpi_proxsensor_init, + .data = &proxsensor_driver_data, + }, + { + .init = tpacpi_dytc_profile_init, + .data = &dytc_profile_driver_data, + }, + { + .init = tpacpi_kbdlang_init, + .data = &kbdlang_driver_data, + }, + { + .init = tpacpi_dprc_init, + .data = &dprc_driver_data, + }, + { + .init = auxmac_init, + .data = &auxmac_data, + }, +}; + +static int __init set_ibm_param(const char *val, const struct kernel_param *kp) +{ + unsigned int i; + struct ibm_struct *ibm; + + if (!kp || !kp->name || !val) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ibm = ibms_init[i].data; + if (!ibm || !ibm->name) + continue; + + if (strcmp(ibm->name, kp->name) == 0 && ibm->write) { + if (strlen(val) > sizeof(ibms_init[i].param) - 1) + return -ENOSPC; + strscpy(ibms_init[i].param, val); + return 0; + } + } + + return -EINVAL; +} + +module_param(experimental, int, 0444); +MODULE_PARM_DESC(experimental, + "Enables experimental features when non-zero"); + +module_param_named(debug, dbg_level, uint, 0); +MODULE_PARM_DESC(debug, "Sets debug level bit-mask"); + +module_param(force_load, bool, 0444); +MODULE_PARM_DESC(force_load, + "Attempts to load the driver even on a mis-identified ThinkPad when true"); + +module_param_named(fan_control, fan_control_allowed, bool, 0444); +MODULE_PARM_DESC(fan_control, + "Enables setting fan parameters features when true"); + +module_param_named(brightness_mode, brightness_mode, uint, 0444); +MODULE_PARM_DESC(brightness_mode, + "Selects brightness control strategy: 0=auto, 1=EC, 2=UCMS, 3=EC+NVRAM"); + +module_param(brightness_enable, uint, 0444); +MODULE_PARM_DESC(brightness_enable, + "Enables backlight control when 1, disables when 0"); + +#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT +module_param_named(volume_mode, volume_mode, uint, 0444); +MODULE_PARM_DESC(volume_mode, + "Selects volume control strategy: 0=auto, 1=EC, 2=N/A, 3=EC+NVRAM"); + +module_param_named(volume_capabilities, volume_capabilities, uint, 0444); +MODULE_PARM_DESC(volume_capabilities, + "Selects the mixer capabilities: 0=auto, 1=volume and mute, 2=mute only"); + +module_param_named(volume_control, volume_control_allowed, bool, 0444); +MODULE_PARM_DESC(volume_control, + "Enables software override for the console audio control when true"); + +module_param_named(software_mute, software_mute_requested, bool, 0444); +MODULE_PARM_DESC(software_mute, + "Request full software mute control"); + +/* ALSA module API parameters */ +module_param_named(index, alsa_index, int, 0444); +MODULE_PARM_DESC(index, "ALSA index for the ACPI EC Mixer"); +module_param_named(id, alsa_id, charp, 0444); +MODULE_PARM_DESC(id, "ALSA id for the ACPI EC Mixer"); +module_param_named(enable, alsa_enable, bool, 0444); +MODULE_PARM_DESC(enable, "Enable the ALSA interface for the ACPI EC Mixer"); +#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ + +/* The module parameter can't be read back, that's why 0 is used here */ +#define TPACPI_PARAM(feature) \ + module_param_call(feature, set_ibm_param, NULL, NULL, 0); \ + MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command at module load, see documentation") + +TPACPI_PARAM(hotkey); +TPACPI_PARAM(bluetooth); +TPACPI_PARAM(video); +TPACPI_PARAM(light); +TPACPI_PARAM(cmos); +TPACPI_PARAM(led); +TPACPI_PARAM(beep); +TPACPI_PARAM(brightness); +TPACPI_PARAM(volume); +TPACPI_PARAM(fan); + +#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES +module_param(dbg_wlswemul, uint, 0444); +MODULE_PARM_DESC(dbg_wlswemul, "Enables WLSW emulation"); +module_param_named(wlsw_state, tpacpi_wlsw_emulstate, bool, 0); +MODULE_PARM_DESC(wlsw_state, + "Initial state of the emulated WLSW switch"); + +module_param(dbg_bluetoothemul, uint, 0444); +MODULE_PARM_DESC(dbg_bluetoothemul, "Enables bluetooth switch emulation"); +module_param_named(bluetooth_state, tpacpi_bluetooth_emulstate, bool, 0); +MODULE_PARM_DESC(bluetooth_state, + "Initial state of the emulated bluetooth switch"); + +module_param(dbg_wwanemul, uint, 0444); +MODULE_PARM_DESC(dbg_wwanemul, "Enables WWAN switch emulation"); +module_param_named(wwan_state, tpacpi_wwan_emulstate, bool, 0); +MODULE_PARM_DESC(wwan_state, + "Initial state of the emulated WWAN switch"); + +module_param(dbg_uwbemul, uint, 0444); +MODULE_PARM_DESC(dbg_uwbemul, "Enables UWB switch emulation"); +module_param_named(uwb_state, tpacpi_uwb_emulstate, bool, 0); +MODULE_PARM_DESC(uwb_state, + "Initial state of the emulated UWB switch"); +#endif + +module_param(profile_force, int, 0444); +MODULE_PARM_DESC(profile_force, "Force profile mode. -1=off, 1=MMC, 2=PSC"); + +static void thinkpad_acpi_module_exit(void) +{ + tpacpi_lifecycle = TPACPI_LIFE_EXITING; + + if (tpacpi_sensors_pdev) { + platform_driver_unregister(&tpacpi_hwmon_pdriver); + platform_device_unregister(tpacpi_sensors_pdev); + } + + if (tp_features.platform_drv_registered) + platform_driver_unregister(&tpacpi_pdriver); + if (tpacpi_pdev) + platform_device_unregister(tpacpi_pdev); + + if (proc_dir) + remove_proc_entry(TPACPI_PROC_DIR, acpi_root_dir); + if (tpacpi_wq) + destroy_workqueue(tpacpi_wq); + + kfree(thinkpad_id.bios_version_str); + kfree(thinkpad_id.ec_version_str); + kfree(thinkpad_id.model_str); + kfree(thinkpad_id.nummodel_str); +} + +static void tpacpi_subdrivers_release(void *data) +{ + struct ibm_struct *ibm, *itmp; + + list_for_each_entry_safe_reverse(ibm, itmp, &tpacpi_all_drivers, all_drivers) + ibm_exit(ibm); + + dbg_printk(TPACPI_DBG_INIT, "finished subdriver exit path...\n"); +} + +static int __init tpacpi_pdriver_probe(struct platform_device *pdev) +{ + int ret; + + ret = devm_mutex_init(&pdev->dev, &tpacpi_inputdev_send_mutex); + if (ret) + return ret; + + tpacpi_inputdev = devm_input_allocate_device(&pdev->dev); + if (!tpacpi_inputdev) + return -ENOMEM; + + tpacpi_inputdev->name = "ThinkPad Extra Buttons"; + tpacpi_inputdev->phys = TPACPI_DRVR_NAME "/input0"; + tpacpi_inputdev->id.bustype = BUS_HOST; + tpacpi_inputdev->id.vendor = thinkpad_id.vendor; + tpacpi_inputdev->id.product = TPACPI_HKEY_INPUT_PRODUCT; + tpacpi_inputdev->id.version = TPACPI_HKEY_INPUT_VERSION; + tpacpi_inputdev->dev.parent = &tpacpi_pdev->dev; + + /* Init subdriver dependencies */ + tpacpi_detect_brightness_capabilities(); + + /* Init subdrivers */ + for (unsigned int i = 0; i < ARRAY_SIZE(ibms_init); i++) { + ret = ibm_init(&ibms_init[i]); + if (ret >= 0 && *ibms_init[i].param) + ret = ibms_init[i].data->write(ibms_init[i].param); + if (ret < 0) { + tpacpi_subdrivers_release(NULL); + return ret; + } + } + + ret = devm_add_action_or_reset(&pdev->dev, tpacpi_subdrivers_release, NULL); + if (ret) + return ret; + + ret = input_register_device(tpacpi_inputdev); + if (ret < 0) + pr_err("unable to register input device\n"); + + return ret; +} + +static int __init tpacpi_hwmon_pdriver_probe(struct platform_device *pdev) +{ + tpacpi_hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, TPACPI_NAME, + NULL, tpacpi_hwmon_groups); + if (IS_ERR(tpacpi_hwmon)) + pr_err("unable to register hwmon device\n"); + + return PTR_ERR_OR_ZERO(tpacpi_hwmon); +} + +static int __init thinkpad_acpi_module_init(void) +{ + const struct dmi_system_id *dmi_id; + int ret; + acpi_object_type obj_type; + + tpacpi_lifecycle = TPACPI_LIFE_INIT; + + /* Driver-level probe */ + + ret = get_thinkpad_model_data(&thinkpad_id); + if (ret) { + pr_err("unable to get DMI data: %d\n", ret); + thinkpad_acpi_module_exit(); + return ret; + } + ret = probe_for_thinkpad(); + if (ret) { + thinkpad_acpi_module_exit(); + return ret; + } + + /* Driver initialization */ + + thinkpad_acpi_init_banner(); + tpacpi_check_outdated_fw(); + + TPACPI_ACPIHANDLE_INIT(ecrd); + TPACPI_ACPIHANDLE_INIT(ecwr); + + /* + * Quirk: in some models (e.g. X380 Yoga), an object named ECRD + * exists, but it is a register, not a method. + */ + if (ecrd_handle) { + acpi_get_type(ecrd_handle, &obj_type); + if (obj_type != ACPI_TYPE_METHOD) + ecrd_handle = NULL; + } + if (ecwr_handle) { + acpi_get_type(ecwr_handle, &obj_type); + if (obj_type != ACPI_TYPE_METHOD) + ecwr_handle = NULL; + } + + tpacpi_wq = create_singlethread_workqueue(TPACPI_WORKQUEUE_NAME); + if (!tpacpi_wq) { + thinkpad_acpi_module_exit(); + return -ENOMEM; + } + + proc_dir = proc_mkdir(TPACPI_PROC_DIR, acpi_root_dir); + if (!proc_dir) { + pr_err("unable to create proc dir " TPACPI_PROC_DIR "\n"); + thinkpad_acpi_module_exit(); + return -ENODEV; + } + + dmi_id = dmi_first_match(fwbug_list); + if (dmi_id) + tp_features.quirks = dmi_id->driver_data; + + /* Device initialization */ + tpacpi_pdev = platform_device_register_simple(TPACPI_DRVR_NAME, PLATFORM_DEVID_NONE, + NULL, 0); + if (IS_ERR(tpacpi_pdev)) { + ret = PTR_ERR(tpacpi_pdev); + tpacpi_pdev = NULL; + pr_err("unable to register platform device\n"); + thinkpad_acpi_module_exit(); + return ret; + } + + ret = platform_driver_probe(&tpacpi_pdriver, tpacpi_pdriver_probe); + if (ret) { + pr_err("unable to register main platform driver\n"); + thinkpad_acpi_module_exit(); + return ret; + } + tp_features.platform_drv_registered = 1; + + tpacpi_sensors_pdev = platform_create_bundle(&tpacpi_hwmon_pdriver, + tpacpi_hwmon_pdriver_probe, + NULL, 0, NULL, 0); + if (IS_ERR(tpacpi_sensors_pdev)) { + ret = PTR_ERR(tpacpi_sensors_pdev); + tpacpi_sensors_pdev = NULL; + pr_err("unable to register hwmon platform device/driver bundle\n"); + thinkpad_acpi_module_exit(); + return ret; + } + + tpacpi_lifecycle = TPACPI_LIFE_RUNNING; + + return 0; +} + +MODULE_ALIAS(TPACPI_DRVR_SHORTNAME); + +/* + * This will autoload the driver in almost every ThinkPad + * in widespread use. + * + * Only _VERY_ old models, like the 240, 240x and 570 lack + * the HKEY event interface. + */ +MODULE_DEVICE_TABLE(acpi, ibm_htk_device_ids); + +/* + * DMI matching for module autoloading + * + * See https://thinkwiki.org/wiki/List_of_DMI_IDs + * See https://thinkwiki.org/wiki/BIOS_Upgrade_Downloads + * + * Only models listed in thinkwiki will be supported, so add yours + * if it is not there yet. + */ +#define IBM_BIOS_MODULE_ALIAS(__type) \ + MODULE_ALIAS("dmi:bvnIBM:bvr" __type "ET??WW*") + +/* Ancient thinkpad BIOSes have to be identified by + * BIOS type or model number, and there are far less + * BIOS types than model numbers... */ +IBM_BIOS_MODULE_ALIAS("I[MU]"); /* 570, 570e */ + +MODULE_AUTHOR("Borislav Deianov "); +MODULE_AUTHOR("Henrique de Moraes Holschuh "); +MODULE_DESCRIPTION(TPACPI_DESC); +MODULE_VERSION(TPACPI_VERSION); +MODULE_LICENSE("GPL"); + +module_init(thinkpad_acpi_module_init); +module_exit(thinkpad_acpi_module_exit); diff --git a/drivers/platform/x86/lenovo/wmi-camera.c b/drivers/platform/x86/lenovo/wmi-camera.c new file mode 100644 index 000000000000..eb60fb9a5b3f --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-camera.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Lenovo WMI Camera Button Driver + * + * Author: Ai Chao + * Copyright (C) 2024 KylinSoft Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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, + .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 "); +MODULE_DESCRIPTION("Lenovo WMI Camera Button Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c b/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c new file mode 100644 index 000000000000..89153afd7015 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-hotkey-utilities.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Lenovo Super Hotkey Utility WMI extras driver for Ideapad laptop + * + * Copyright (C) 2025 Lenovo + */ + +#include +#include +#include +#include +#include +#include + +/* 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) + led_version = obj->integer.value; + else + return -EIO; + + wpriv->cdev[led_type].max_brightness = LED_ON; + wpriv->cdev[led_type].flags = LED_CORE_SUSPENDRESUME; + + switch (led_type) { + case MIC_MUTE: + if (led_version != WMI_LUD_SUPPORT_MICMUTE_LED_VER) + return -EIO; + + 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) + return -EIO; + + 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; + } + + 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 "); +MODULE_DESCRIPTION("Lenovo Super Hotkey Utility WMI extras driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/ymc.c b/drivers/platform/x86/lenovo/ymc.c new file mode 100644 index 000000000000..470d53e3c9d2 --- /dev/null +++ b/drivers/platform/x86/lenovo/ymc.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * lenovo-ymc.c - Lenovo Yoga Mode Control driver + * + * Copyright © 2022 Gergo Koteles + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#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, + .probe = lenovo_ymc_probe, + .notify = lenovo_ymc_notify, +}; + +module_wmi_driver(lenovo_ymc_driver); + +MODULE_AUTHOR("Gergo Koteles "); +MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("IDEAPAD_LAPTOP"); diff --git a/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c new file mode 100644 index 000000000000..b3fd6a35052a --- /dev/null +++ b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c @@ -0,0 +1,339 @@ +// 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 + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 = pinctrl_register_mappings(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)) { + ret = PTR_ERR(ctrl_dev); + goto out_pinctrl_unregister_mappings; + } + + serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); + put_device(ctrl_dev); + if (!serdev) { + ret = -ENOMEM; + goto out_pinctrl_unregister_mappings; + } + + ret = serdev_device_add(serdev); + if (ret) { + dev_err_probe(&pdev->dev, ret, "adding serdev\n"); + serdev_device_put(serdev); + goto out_pinctrl_unregister_mappings; + } + + /* + * 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 */ + ret = (ret == -EAGAIN) ? -EPROBE_DEFER : ret; + dev_err_probe(&pdev->dev, ret, "attaching serdev driver\n"); + goto out_serdev_device_remove; + } + + /* So that yt2_1380_fc_pdev_remove() can remove the serdev */ + platform_set_drvdata(pdev, serdev); + return 0; + +out_serdev_device_remove: + serdev_device_remove(serdev); +out_pinctrl_unregister_mappings: + pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); + return ret; +} + +static void yt2_1380_fc_pdev_remove(struct platform_device *pdev) +{ + struct serdev_device *serdev = platform_get_drvdata(pdev); + + serdev_device_remove(serdev); + pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); +} + +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 "); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/yogabook.c b/drivers/platform/x86/lenovo/yogabook.c new file mode 100644 index 000000000000..31b298dc5046 --- /dev/null +++ b/drivers/platform/x86/lenovo/yogabook.c @@ -0,0 +1,573 @@ +// 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + uint8_t 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 = ¶m; + + 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 == NULL) + 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), + }, + .no_notify_data = true, + .id_table = yogabook_wmi_id_table, + .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 == NULL) + 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"); diff --git a/drivers/platform/x86/think-lmi.c b/drivers/platform/x86/think-lmi.c deleted file mode 100644 index 02ede1ec99e9..000000000000 --- a/drivers/platform/x86/think-lmi.c +++ /dev/null @@ -1,1821 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Think LMI BIOS configuration driver - * - * Copyright(C) 2019-2021 Lenovo - * - * Original code from Thinkpad-wmi project https://github.com/iksaif/thinkpad-wmi - * Copyright(C) 2017 Corentin Chary - * Distributed under the GPL-2.0 license - */ - -#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "firmware_attributes_class.h" -#include "think-lmi.h" - -static bool debug_support; -module_param(debug_support, bool, 0444); -MODULE_PARM_DESC(debug_support, "Enable debug command support"); - -/* - * Name: BiosSetting - * Description: Get item name and settings for current LMI instance. - * Type: Query - * Returns: "Item,Value" - * Example: "WakeOnLAN,Enable" - */ -#define LENOVO_BIOS_SETTING_GUID "51F5230E-9677-46CD-A1CF-C0B23EE34DB7" - -/* - * Name: SetBiosSetting - * Description: Change the BIOS setting to the desired value using the SetBiosSetting - * class. To save the settings, use the SaveBiosSetting class. - * BIOS settings and values are case sensitive. - * After making changes to the BIOS settings, you must reboot the computer - * before the changes will take effect. - * Type: Method - * Arguments: "Item,Value,Password,Encoding,KbdLang;" - * Example: "WakeOnLAN,Disable,pa55w0rd,ascii,us;" - */ -#define LENOVO_SET_BIOS_SETTINGS_GUID "98479A64-33F5-4E33-A707-8E251EBBC3A1" - -/* - * Name: SaveBiosSettings - * Description: Save any pending changes in settings. - * Type: Method - * Arguments: "Password,Encoding,KbdLang;" - * Example: "pa55w0rd,ascii,us;" - */ -#define LENOVO_SAVE_BIOS_SETTINGS_GUID "6A4B54EF-A5ED-4D33-9455-B0D9B48DF4B3" - -/* - * Name: BiosPasswordSettings - * Description: Return BIOS Password settings - * Type: Query - * Returns: PasswordMode, PasswordState, MinLength, MaxLength, - * SupportedEncoding, SupportedKeyboard - */ -#define LENOVO_BIOS_PASSWORD_SETTINGS_GUID "8ADB159E-1E32-455C-BC93-308A7ED98246" - -/* - * Name: SetBiosPassword - * Description: Change a specific password. - * - BIOS settings cannot be changed at the same boot as power-on - * passwords (POP) and hard disk passwords (HDP). If you want to change - * BIOS settings and POP or HDP, you must reboot the system after changing - * one of them. - * - A password cannot be set using this method when one does not already - * exist. Passwords can only be updated or cleared. - * Type: Method - * Arguments: "PasswordType,CurrentPassword,NewPassword,Encoding,KbdLang;" - * Example: "pop,pa55w0rd,newpa55w0rd,ascii,us;” - */ -#define LENOVO_SET_BIOS_PASSWORD_GUID "2651D9FD-911C-4B69-B94E-D0DED5963BD7" - -/* - * Name: GetBiosSelections - * Description: Return a list of valid settings for a given item. - * Type: Method - * Arguments: "Item" - * Returns: "Value1,Value2,Value3,..." - * Example: - * -> "FlashOverLAN" - * <- "Enabled,Disabled" - */ -#define LENOVO_GET_BIOS_SELECTIONS_GUID "7364651A-132F-4FE7-ADAA-40C6C7EE2E3B" - -/* - * Name: DebugCmd - * Description: Debug entry method for entering debug commands to the BIOS - */ -#define LENOVO_DEBUG_CMD_GUID "7FF47003-3B6C-4E5E-A227-E979824A85D1" - -/* - * Name: OpcodeIF - * Description: Opcode interface which provides the ability to set multiple - * parameters and then trigger an action with a final command. - * This is particularly useful for simplifying setting passwords. - * With this support comes the ability to set System, HDD and NVMe - * passwords. - * This is currently available on ThinkCenter and ThinkStations platforms - */ -#define LENOVO_OPCODE_IF_GUID "DFDDEF2C-57D4-48ce-B196-0FB787D90836" - -/* - * Name: SetBiosCert - * Description: Install BIOS certificate. - * Type: Method - * Arguments: "Certificate,Password" - * You must reboot the computer before the changes will take effect. - */ -#define LENOVO_SET_BIOS_CERT_GUID "26861C9F-47E9-44C4-BD8B-DFE7FA2610FE" - -/* - * Name: UpdateBiosCert - * Description: Update BIOS certificate. - * Type: Method - * Format: "Certificate,Signature" - * You must reboot the computer before the changes will take effect. - */ -#define LENOVO_UPDATE_BIOS_CERT_GUID "9AA3180A-9750-41F7-B9F7-D5D3B1BAC3CE" - -/* - * Name: ClearBiosCert - * Description: Uninstall BIOS certificate. - * Type: Method - * Format: "Serial,Signature" - * You must reboot the computer before the changes will take effect. - */ -#define LENOVO_CLEAR_BIOS_CERT_GUID "B2BC39A7-78DD-4D71-B059-A510DEC44890" -/* - * Name: CertToPassword - * Description: Switch from certificate to password authentication. - * Type: Method - * Format: "Password,Signature" - * You must reboot the computer before the changes will take effect. - */ -#define LENOVO_CERT_TO_PASSWORD_GUID "0DE8590D-5510-4044-9621-77C227F5A70D" - -/* - * Name: SetBiosSettingCert - * Description: Set attribute using certificate authentication. - * Type: Method - * Format: "Item,Value,Signature" - */ -#define LENOVO_SET_BIOS_SETTING_CERT_GUID "34A008CC-D205-4B62-9E67-31DFA8B90003" - -/* - * Name: SaveBiosSettingCert - * Description: Save any pending changes in settings. - * Type: Method - * Format: "Signature" - */ -#define LENOVO_SAVE_BIOS_SETTING_CERT_GUID "C050FB9D-DF5F-4606-B066-9EFC401B2551" - -/* - * Name: CertThumbprint - * Description: Display Certificate thumbprints - * Type: Query - * Returns: MD5, SHA1 & SHA256 thumbprints - */ -#define LENOVO_CERT_THUMBPRINT_GUID "C59119ED-1C0D-4806-A8E9-59AA318176C4" - -#define TLMI_POP_PWD BIT(0) /* Supervisor */ -#define TLMI_PAP_PWD BIT(1) /* Power-on */ -#define TLMI_HDD_PWD BIT(2) /* HDD/NVME */ -#define TLMI_SMP_PWD BIT(6) /* System Management */ -#define TLMI_CERT_SVC BIT(7) /* Admin Certificate Based */ -#define TLMI_CERT_SMC BIT(8) /* System Certificate Based */ - -static const struct tlmi_err_codes tlmi_errs[] = { - {"Success", 0}, - {"Not Supported", -EOPNOTSUPP}, - {"Invalid Parameter", -EINVAL}, - {"Access Denied", -EACCES}, - {"System Busy", -EBUSY}, -}; - -static const char * const encoding_options[] = { - [TLMI_ENCODING_ASCII] = "ascii", - [TLMI_ENCODING_SCANCODE] = "scancode", -}; -static const char * const level_options[] = { - [TLMI_LEVEL_USER] = "user", - [TLMI_LEVEL_MASTER] = "master", -}; -static struct think_lmi tlmi_priv; -static DEFINE_MUTEX(tlmi_mutex); - -static inline struct tlmi_pwd_setting *to_tlmi_pwd_setting(struct kobject *kobj) -{ - return container_of(kobj, struct tlmi_pwd_setting, kobj); -} - -static inline struct tlmi_attr_setting *to_tlmi_attr_setting(struct kobject *kobj) -{ - return container_of(kobj, struct tlmi_attr_setting, kobj); -} - -/* Convert BIOS WMI error string to suitable error code */ -static int tlmi_errstr_to_err(const char *errstr) -{ - int i; - - for (i = 0; i < sizeof(tlmi_errs)/sizeof(struct tlmi_err_codes); i++) { - if (!strcmp(tlmi_errs[i].err_str, errstr)) - return tlmi_errs[i].err_code; - } - return -EPERM; -} - -/* Extract error string from WMI return buffer */ -static int tlmi_extract_error(const struct acpi_buffer *output) -{ - const union acpi_object *obj; - - obj = output->pointer; - if (!obj) - return -ENOMEM; - if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) - return -EIO; - - return tlmi_errstr_to_err(obj->string.pointer); -} - -/* Utility function to execute WMI call to BIOS */ -static int tlmi_simple_call(const char *guid, const char *arg) -{ - const struct acpi_buffer input = { strlen(arg), (char *)arg }; - struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; - acpi_status status; - int i, err; - - /* - * Duplicated call required to match BIOS workaround for behavior - * seen when WMI accessed via scripting on other OS. - */ - for (i = 0; i < 2; i++) { - /* (re)initialize output buffer to default state */ - output.length = ACPI_ALLOCATE_BUFFER; - output.pointer = NULL; - - status = wmi_evaluate_method(guid, 0, 0, &input, &output); - if (ACPI_FAILURE(status)) { - kfree(output.pointer); - return -EIO; - } - err = tlmi_extract_error(&output); - kfree(output.pointer); - if (err) - return err; - } - return 0; -} - -/* Extract output string from WMI return value */ -static int tlmi_extract_output_string(union acpi_object *obj, char **string) -{ - char *s; - - if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) - return -EIO; - - s = kstrdup(obj->string.pointer, GFP_KERNEL); - if (!s) - return -ENOMEM; - *string = s; - return 0; -} - -/* ------ Core interface functions ------------*/ - -/* Get password settings from BIOS */ -static int tlmi_get_pwd_settings(struct tlmi_pwdcfg *pwdcfg) -{ - struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; - const union acpi_object *obj; - acpi_status status; - int copy_size; - - if (!tlmi_priv.can_get_password_settings) - return -EOPNOTSUPP; - - status = wmi_query_block(LENOVO_BIOS_PASSWORD_SETTINGS_GUID, 0, - &output); - if (ACPI_FAILURE(status)) - return -EIO; - - obj = output.pointer; - if (!obj) - return -ENOMEM; - if (obj->type != ACPI_TYPE_BUFFER || !obj->buffer.pointer) { - kfree(obj); - return -EIO; - } - /* - * The size of thinkpad_wmi_pcfg on ThinkStation is larger than ThinkPad. - * To make the driver compatible on different brands, we permit it to get - * the data in below case. - * Settings must have at minimum the core fields available - */ - if (obj->buffer.length < sizeof(struct tlmi_pwdcfg_core)) { - pr_warn("Unknown pwdcfg buffer length %d\n", obj->buffer.length); - kfree(obj); - return -EIO; - } - - copy_size = min_t(size_t, obj->buffer.length, sizeof(struct tlmi_pwdcfg)); - - memcpy(pwdcfg, obj->buffer.pointer, copy_size); - kfree(obj); - - if (WARN_ON(pwdcfg->core.max_length >= TLMI_PWD_BUFSIZE)) - pwdcfg->core.max_length = TLMI_PWD_BUFSIZE - 1; - return 0; -} - -static int tlmi_save_bios_settings(const char *password) -{ - return tlmi_simple_call(LENOVO_SAVE_BIOS_SETTINGS_GUID, - password); -} - -static int tlmi_opcode_setting(char *setting, const char *value) -{ - char *opcode_str; - int ret; - - opcode_str = kasprintf(GFP_KERNEL, "%s:%s;", setting, value); - if (!opcode_str) - return -ENOMEM; - - ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, opcode_str); - kfree(opcode_str); - return ret; -} - -static int tlmi_setting(struct wmi_device *wdev, int item, char **value) -{ - union acpi_object *obj; - int ret; - - obj = wmidev_block_query(wdev, item); - if (!obj) - return -EIO; - - ret = tlmi_extract_output_string(obj, value); - kfree(obj); - - return ret; -} - -static int tlmi_get_bios_selections(const char *item, char **value) -{ - const struct acpi_buffer input = { strlen(item), (char *)item }; - struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; - union acpi_object *obj; - acpi_status status; - int ret; - - status = wmi_evaluate_method(LENOVO_GET_BIOS_SELECTIONS_GUID, - 0, 0, &input, &output); - if (ACPI_FAILURE(status)) - return -EIO; - - obj = output.pointer; - if (!obj) - return -ENODATA; - - ret = tlmi_extract_output_string(obj, value); - kfree(obj); - - return ret; -} - -/* ---- Authentication sysfs --------------------------------------------------------- */ -static ssize_t is_enabled_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%d\n", setting->pwd_enabled || setting->cert_installed); -} - -static struct kobj_attribute auth_is_pass_set = __ATTR_RO(is_enabled); - -static ssize_t current_password_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - size_t pwdlen; - - pwdlen = strlen(buf); - /* pwdlen == 0 is allowed to clear the password */ - if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) - return -EINVAL; - - strscpy(setting->password, buf, setting->maxlen); - /* Strip out CR if one is present, setting password won't work if it is present */ - strreplace(setting->password, '\n', '\0'); - return count; -} - -static struct kobj_attribute auth_current_password = __ATTR_WO(current_password); - -static ssize_t new_password_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - char *auth_str, *new_pwd; - size_t pwdlen; - int ret; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - if (!tlmi_priv.can_set_bios_password) - return -EOPNOTSUPP; - - /* Strip out CR if one is present, setting password won't work if it is present */ - new_pwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_pwd) - return -ENOMEM; - - /* Use lock in case multiple WMI operations needed */ - mutex_lock(&tlmi_mutex); - - pwdlen = strlen(new_pwd); - /* pwdlen == 0 is allowed to clear the password */ - if (pwdlen && ((pwdlen < setting->minlen) || (pwdlen > setting->maxlen))) { - ret = -EINVAL; - goto out; - } - - /* If opcode support is present use that interface */ - if (tlmi_priv.opcode_support) { - char pwd_type[8]; - - /* Special handling required for HDD and NVMe passwords */ - if (setting == tlmi_priv.pwd_hdd) { - if (setting->level == TLMI_LEVEL_USER) - sprintf(pwd_type, "uhdp%d", setting->index); - else - sprintf(pwd_type, "mhdp%d", setting->index); - } else if (setting == tlmi_priv.pwd_nvme) { - if (setting->level == TLMI_LEVEL_USER) - sprintf(pwd_type, "udrp%d", setting->index); - else - sprintf(pwd_type, "adrp%d", setting->index); - } else { - sprintf(pwd_type, "%s", setting->pwd_type); - } - - ret = tlmi_opcode_setting("WmiOpcodePasswordType", pwd_type); - if (ret) - goto out; - - /* - * Note admin password is not always required if SMPControl enabled in BIOS, - * So only set if it's configured. - * Let BIOS figure it out - we'll get an error if operation is not permitted - */ - if (tlmi_priv.pwd_admin->pwd_enabled && strlen(tlmi_priv.pwd_admin->password)) { - ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", - tlmi_priv.pwd_admin->password); - if (ret) - goto out; - } - ret = tlmi_opcode_setting("WmiOpcodePasswordCurrent01", setting->password); - if (ret) - goto out; - ret = tlmi_opcode_setting("WmiOpcodePasswordNew01", new_pwd); - if (ret) - goto out; - ret = tlmi_simple_call(LENOVO_OPCODE_IF_GUID, "WmiOpcodePasswordSetUpdate;"); - } else { - /* Format: 'PasswordType,CurrentPw,NewPw,Encoding,KbdLang;' */ - auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s,%s,%s;", - setting->pwd_type, setting->password, new_pwd, - encoding_options[setting->encoding], setting->kbdlang); - if (!auth_str) { - ret = -ENOMEM; - goto out; - } - ret = tlmi_simple_call(LENOVO_SET_BIOS_PASSWORD_GUID, auth_str); - kfree(auth_str); - } -out: - mutex_unlock(&tlmi_mutex); - kfree(new_pwd); - return ret ?: count; -} - -static struct kobj_attribute auth_new_password = __ATTR_WO(new_password); - -static ssize_t min_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%d\n", setting->minlen); -} - -static struct kobj_attribute auth_min_pass_length = __ATTR_RO(min_password_length); - -static ssize_t max_password_length_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%d\n", setting->maxlen); -} -static struct kobj_attribute auth_max_pass_length = __ATTR_RO(max_password_length); - -static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - if (setting->cert_installed) - return sysfs_emit(buf, "certificate\n"); - return sysfs_emit(buf, "password\n"); -} -static struct kobj_attribute auth_mechanism = __ATTR_RO(mechanism); - -static ssize_t encoding_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%s\n", encoding_options[setting->encoding]); -} - -static ssize_t encoding_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - int i; - - /* Scan for a matching profile */ - i = sysfs_match_string(encoding_options, buf); - if (i < 0) - return -EINVAL; - - setting->encoding = i; - return count; -} - -static struct kobj_attribute auth_encoding = __ATTR_RW(encoding); - -static ssize_t kbdlang_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%s\n", setting->kbdlang); -} - -static ssize_t kbdlang_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - int length; - - /* Calculate length till '\n' or terminating 0 */ - length = strchrnul(buf, '\n') - buf; - if (!length || length >= TLMI_LANG_MAXLEN) - return -EINVAL; - - memcpy(setting->kbdlang, buf, length); - setting->kbdlang[length] = '\0'; - return count; -} - -static struct kobj_attribute auth_kbdlang = __ATTR_RW(kbdlang); - -static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%s\n", setting->role); -} -static struct kobj_attribute auth_role = __ATTR_RO(role); - -static ssize_t index_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%d\n", setting->index); -} - -static ssize_t index_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - int err, val; - - err = kstrtoint(buf, 10, &val); - if (err < 0) - return err; - - if (val < 0 || val > TLMI_INDEX_MAX) - return -EINVAL; - - setting->index = val; - return count; -} - -static struct kobj_attribute auth_index = __ATTR_RW(index); - -static ssize_t level_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - return sysfs_emit(buf, "%s\n", level_options[setting->level]); -} - -static ssize_t level_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - int i; - - /* Scan for a matching profile */ - i = sysfs_match_string(level_options, buf); - if (i < 0) - return -EINVAL; - - setting->level = i; - return count; -} - -static struct kobj_attribute auth_level = __ATTR_RW(level); - -static char *cert_command(struct tlmi_pwd_setting *setting, const char *arg1, const char *arg2) -{ - /* Prepend with SVC or SMC if multicert supported */ - if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) - return kasprintf(GFP_KERNEL, "%s,%s,%s", - setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", - arg1, arg2); - else - return kasprintf(GFP_KERNEL, "%s,%s", arg1, arg2); -} - -static ssize_t cert_thumbprint(char *buf, const char *arg, int count) -{ - const struct acpi_buffer input = { strlen(arg), (char *)arg }; - struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; - const union acpi_object *obj; - acpi_status status; - - status = wmi_evaluate_method(LENOVO_CERT_THUMBPRINT_GUID, 0, 0, &input, &output); - if (ACPI_FAILURE(status)) { - kfree(output.pointer); - return -EIO; - } - obj = output.pointer; - if (!obj) - return -ENOMEM; - if (obj->type != ACPI_TYPE_STRING || !obj->string.pointer) { - kfree(output.pointer); - return -EIO; - } - count += sysfs_emit_at(buf, count, "%s : %s\n", arg, (char *)obj->string.pointer); - kfree(output.pointer); - - return count; -} - -static char *thumbtypes[] = {"Md5", "Sha1", "Sha256"}; - -static ssize_t certificate_thumbprint_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - unsigned int i; - int count = 0; - char *wmistr; - - if (!tlmi_priv.certificate_support || !setting->cert_installed) - return -EOPNOTSUPP; - - for (i = 0; i < ARRAY_SIZE(thumbtypes); i++) { - if (tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) { - /* Format: 'SVC | SMC, Thumbtype' */ - wmistr = kasprintf(GFP_KERNEL, "%s,%s", - setting == tlmi_priv.pwd_admin ? "SVC" : "SMC", - thumbtypes[i]); - } else { - /* Format: 'Thumbtype' */ - wmistr = kasprintf(GFP_KERNEL, "%s", thumbtypes[i]); - } - if (!wmistr) - return -ENOMEM; - count += cert_thumbprint(buf, wmistr, count); - kfree(wmistr); - } - - return count; -} - -static struct kobj_attribute auth_cert_thumb = __ATTR_RO(certificate_thumbprint); - -static ssize_t cert_to_password_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - char *auth_str, *passwd; - int ret; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - if (!tlmi_priv.certificate_support) - return -EOPNOTSUPP; - - if (!setting->cert_installed) - return -EINVAL; - - if (!setting->signature || !setting->signature[0]) - return -EACCES; - - /* Strip out CR if one is present */ - passwd = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!passwd) - return -ENOMEM; - - /* Format: 'Password,Signature' */ - auth_str = cert_command(setting, passwd, setting->signature); - if (!auth_str) { - kfree_sensitive(passwd); - return -ENOMEM; - } - ret = tlmi_simple_call(LENOVO_CERT_TO_PASSWORD_GUID, auth_str); - kfree(auth_str); - kfree_sensitive(passwd); - - return ret ?: count; -} - -static struct kobj_attribute auth_cert_to_password = __ATTR_WO(cert_to_password); - -enum cert_install_mode { - TLMI_CERT_INSTALL, - TLMI_CERT_UPDATE, -}; - -static ssize_t certificate_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - enum cert_install_mode install_mode = TLMI_CERT_INSTALL; - char *auth_str, *new_cert; - const char *serial; - char *signature; - char *guid; - int ret; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - if (!tlmi_priv.certificate_support) - return -EOPNOTSUPP; - - /* If empty then clear installed certificate */ - if ((buf[0] == '\0') || (buf[0] == '\n')) { /* Clear installed certificate */ - /* Check that signature is set */ - if (!setting->signature || !setting->signature[0]) - return -EACCES; - - /* Format: 'serial#, signature' */ - serial = dmi_get_system_info(DMI_PRODUCT_SERIAL); - if (!serial) - return -ENODEV; - auth_str = cert_command(setting, serial, setting->signature); - if (!auth_str) - return -ENOMEM; - - ret = tlmi_simple_call(LENOVO_CLEAR_BIOS_CERT_GUID, auth_str); - kfree(auth_str); - - return ret ?: count; - } - - /* Strip out CR if one is present */ - new_cert = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_cert) - return -ENOMEM; - - if (setting->cert_installed) { - /* Certificate is installed so this is an update */ - install_mode = TLMI_CERT_UPDATE; - /* If admin account enabled - need to use its signature */ - if (tlmi_priv.pwd_admin->pwd_enabled) - signature = tlmi_priv.pwd_admin->signature; - else - signature = setting->signature; - } else { /* Cert install */ - /* Check if SMC and SVC already installed */ - if ((setting == tlmi_priv.pwd_system) && tlmi_priv.pwd_admin->cert_installed) { - /* This gets treated as a cert update */ - install_mode = TLMI_CERT_UPDATE; - signature = tlmi_priv.pwd_admin->signature; - } else { /* Regular cert install */ - install_mode = TLMI_CERT_INSTALL; - signature = setting->signature; - } - } - - if (install_mode == TLMI_CERT_UPDATE) { - /* This is a certificate update */ - if (!signature || !signature[0]) { - kfree(new_cert); - return -EACCES; - } - guid = LENOVO_UPDATE_BIOS_CERT_GUID; - /* Format: 'Certificate,Signature' */ - auth_str = cert_command(setting, new_cert, signature); - } else { - /* This is a fresh install */ - /* To set admin cert, a password must be enabled */ - if ((setting == tlmi_priv.pwd_admin) && - (!setting->pwd_enabled || !setting->password[0])) { - kfree(new_cert); - return -EACCES; - } - guid = LENOVO_SET_BIOS_CERT_GUID; - /* Format: 'Certificate, password' */ - auth_str = cert_command(setting, new_cert, setting->password); - } - kfree(new_cert); - if (!auth_str) - return -ENOMEM; - - ret = tlmi_simple_call(guid, auth_str); - kfree(auth_str); - - return ret ?: count; -} - -static struct kobj_attribute auth_certificate = __ATTR_WO(certificate); - -static ssize_t signature_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - char *new_signature; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - if (!tlmi_priv.certificate_support) - return -EOPNOTSUPP; - - /* Strip out CR if one is present */ - new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_signature) - return -ENOMEM; - - /* Free any previous signature */ - kfree(setting->signature); - setting->signature = new_signature; - - return count; -} - -static struct kobj_attribute auth_signature = __ATTR_WO(signature); - -static ssize_t save_signature_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - char *new_signature; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - if (!tlmi_priv.certificate_support) - return -EOPNOTSUPP; - - /* Strip out CR if one is present */ - new_signature = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_signature) - return -ENOMEM; - - /* Free any previous signature */ - kfree(setting->save_signature); - setting->save_signature = new_signature; - - return count; -} - -static struct kobj_attribute auth_save_signature = __ATTR_WO(save_signature); - -static umode_t auth_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - /* We only want to display level and index settings on HDD/NVMe */ - if (attr == &auth_index.attr || attr == &auth_level.attr) { - if ((setting == tlmi_priv.pwd_hdd) || (setting == tlmi_priv.pwd_nvme)) - return attr->mode; - return 0; - } - - /* We only display certificates, if supported */ - if (attr == &auth_certificate.attr || - attr == &auth_signature.attr || - attr == &auth_save_signature.attr || - attr == &auth_cert_thumb.attr || - attr == &auth_cert_to_password.attr) { - if (tlmi_priv.certificate_support) { - if (setting == tlmi_priv.pwd_admin) - return attr->mode; - if ((tlmi_priv.pwdcfg.core.password_mode >= TLMI_PWDCFG_MODE_MULTICERT) && - (setting == tlmi_priv.pwd_system)) - return attr->mode; - } - return 0; - } - - /* Don't display un-needed settings if opcode available */ - if ((attr == &auth_encoding.attr || attr == &auth_kbdlang.attr) && - tlmi_priv.opcode_support) - return 0; - - return attr->mode; -} - -static struct attribute *auth_attrs[] = { - &auth_is_pass_set.attr, - &auth_min_pass_length.attr, - &auth_max_pass_length.attr, - &auth_current_password.attr, - &auth_new_password.attr, - &auth_role.attr, - &auth_mechanism.attr, - &auth_encoding.attr, - &auth_kbdlang.attr, - &auth_index.attr, - &auth_level.attr, - &auth_certificate.attr, - &auth_signature.attr, - &auth_save_signature.attr, - &auth_cert_thumb.attr, - &auth_cert_to_password.attr, - NULL -}; - -static const struct attribute_group auth_attr_group = { - .is_visible = auth_attr_is_visible, - .attrs = auth_attrs, -}; - -/* ---- Attributes sysfs --------------------------------------------------------- */ -static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - - return sysfs_emit(buf, "%s\n", setting->display_name); -} - -static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - char *item, *value; - int ret; - - ret = tlmi_setting(setting->wdev, setting->index, &item); - if (ret) - return ret; - - /* validate and split from `item,value` -> `value` */ - value = strpbrk(item, ","); - if (!value || value == item || !strlen(value + 1)) - ret = -EINVAL; - else { - /* On Workstations remove the Options part after the value */ - strreplace(value, ';', '\0'); - ret = sysfs_emit(buf, "%s\n", value + 1); - } - kfree(item); - - return ret; -} - -static ssize_t possible_values_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - - return sysfs_emit(buf, "%s\n", setting->possible_values); -} - -static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - - if (setting->possible_values) { - /* Figure out what setting type is as BIOS does not return this */ - if (strchr(setting->possible_values, ';')) - return sysfs_emit(buf, "enumeration\n"); - } - /* Anything else is going to be a string */ - return sysfs_emit(buf, "string\n"); -} - -static ssize_t current_value_store(struct kobject *kobj, - struct kobj_attribute *attr, - const char *buf, size_t count) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - char *set_str = NULL, *new_setting = NULL; - char *auth_str = NULL; - int ret; - - if (!tlmi_priv.can_set_bios_settings) - return -EOPNOTSUPP; - - /* - * If we are using bulk saves a reboot should be done once save has - * been called - */ - if (tlmi_priv.save_mode == TLMI_SAVE_BULK && tlmi_priv.reboot_required) - return -EPERM; - - /* Strip out CR if one is present */ - new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_setting) - return -ENOMEM; - - /* Use lock in case multiple WMI operations needed */ - mutex_lock(&tlmi_mutex); - - /* Check if certificate authentication is enabled and active */ - if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { - if (!tlmi_priv.pwd_admin->signature || !tlmi_priv.pwd_admin->save_signature) { - ret = -EINVAL; - goto out; - } - set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, - new_setting, tlmi_priv.pwd_admin->signature); - if (!set_str) { - ret = -ENOMEM; - goto out; - } - - ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTING_CERT_GUID, set_str); - if (ret) - goto out; - if (tlmi_priv.save_mode == TLMI_SAVE_BULK) - tlmi_priv.save_required = true; - else - ret = tlmi_simple_call(LENOVO_SAVE_BIOS_SETTING_CERT_GUID, - tlmi_priv.pwd_admin->save_signature); - } else if (tlmi_priv.opcode_support) { - /* - * If opcode support is present use that interface. - * Note - this sets the variable and then the password as separate - * WMI calls. Function tlmi_save_bios_settings will error if the - * password is incorrect. - * Workstation's require the opcode to be set before changing the - * attribute. - */ - if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { - ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", - tlmi_priv.pwd_admin->password); - if (ret) - goto out; - } - - set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, - new_setting); - if (!set_str) { - ret = -ENOMEM; - goto out; - } - - ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); - if (ret) - goto out; - - if (tlmi_priv.save_mode == TLMI_SAVE_BULK) - tlmi_priv.save_required = true; - else - ret = tlmi_save_bios_settings(""); - } else { /* old non-opcode based authentication method (deprecated) */ - if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { - auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", - tlmi_priv.pwd_admin->password, - encoding_options[tlmi_priv.pwd_admin->encoding], - tlmi_priv.pwd_admin->kbdlang); - if (!auth_str) { - ret = -ENOMEM; - goto out; - } - } - - if (auth_str) - set_str = kasprintf(GFP_KERNEL, "%s,%s,%s", setting->name, - new_setting, auth_str); - else - set_str = kasprintf(GFP_KERNEL, "%s,%s;", setting->name, - new_setting); - if (!set_str) { - ret = -ENOMEM; - goto out; - } - - ret = tlmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, set_str); - if (ret) - goto out; - - if (tlmi_priv.save_mode == TLMI_SAVE_BULK) { - tlmi_priv.save_required = true; - } else { - if (auth_str) - ret = tlmi_save_bios_settings(auth_str); - else - ret = tlmi_save_bios_settings(""); - } - } - if (!ret && !tlmi_priv.pending_changes) { - tlmi_priv.pending_changes = true; - /* let userland know it may need to check reboot pending again */ - kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); - } -out: - mutex_unlock(&tlmi_mutex); - kfree(auth_str); - kfree(set_str); - kfree(new_setting); - return ret ?: count; -} - -static struct kobj_attribute attr_displ_name = __ATTR_RO(display_name); - -static struct kobj_attribute attr_possible_values = __ATTR_RO(possible_values); - -static struct kobj_attribute attr_current_val = __ATTR_RW_MODE(current_value, 0600); - -static struct kobj_attribute attr_type = __ATTR_RO(type); - -static umode_t attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - - /* We don't want to display possible_values attributes if not available */ - if ((attr == &attr_possible_values.attr) && (!setting->possible_values)) - return 0; - - return attr->mode; -} - -static struct attribute *tlmi_attrs[] = { - &attr_displ_name.attr, - &attr_current_val.attr, - &attr_possible_values.attr, - &attr_type.attr, - NULL -}; - -static const struct attribute_group tlmi_attr_group = { - .is_visible = attr_is_visible, - .attrs = tlmi_attrs, -}; - -static void tlmi_attr_setting_release(struct kobject *kobj) -{ - struct tlmi_attr_setting *setting = to_tlmi_attr_setting(kobj); - - kfree(setting->possible_values); - kfree(setting); -} - -static void tlmi_pwd_setting_release(struct kobject *kobj) -{ - struct tlmi_pwd_setting *setting = to_tlmi_pwd_setting(kobj); - - kfree(setting); -} - -static const struct kobj_type tlmi_attr_setting_ktype = { - .release = &tlmi_attr_setting_release, - .sysfs_ops = &kobj_sysfs_ops, -}; - -static const struct kobj_type tlmi_pwd_setting_ktype = { - .release = &tlmi_pwd_setting_release, - .sysfs_ops = &kobj_sysfs_ops, -}; - -static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - return sprintf(buf, "%d\n", tlmi_priv.pending_changes); -} - -static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); - -static const char * const save_mode_strings[] = { - [TLMI_SAVE_SINGLE] = "single", - [TLMI_SAVE_BULK] = "bulk", - [TLMI_SAVE_SAVE] = "save" -}; - -static ssize_t save_settings_show(struct kobject *kobj, struct kobj_attribute *attr, - char *buf) -{ - /* Check that setting is valid */ - if (WARN_ON(tlmi_priv.save_mode < TLMI_SAVE_SINGLE || - tlmi_priv.save_mode > TLMI_SAVE_BULK)) - return -EIO; - return sysfs_emit(buf, "%s\n", save_mode_strings[tlmi_priv.save_mode]); -} - -static ssize_t save_settings_store(struct kobject *kobj, struct kobj_attribute *attr, - const char *buf, size_t count) -{ - char *auth_str = NULL; - int ret = 0; - int cmd; - - cmd = sysfs_match_string(save_mode_strings, buf); - if (cmd < 0) - return cmd; - - /* Use lock in case multiple WMI operations needed */ - mutex_lock(&tlmi_mutex); - - switch (cmd) { - case TLMI_SAVE_SINGLE: - case TLMI_SAVE_BULK: - tlmi_priv.save_mode = cmd; - goto out; - case TLMI_SAVE_SAVE: - /* Check if supported*/ - if (!tlmi_priv.can_set_bios_settings || - tlmi_priv.save_mode == TLMI_SAVE_SINGLE) { - ret = -EOPNOTSUPP; - goto out; - } - /* Check there is actually something to save */ - if (!tlmi_priv.save_required) { - ret = -ENOENT; - goto out; - } - /* Check if certificate authentication is enabled and active */ - if (tlmi_priv.certificate_support && tlmi_priv.pwd_admin->cert_installed) { - if (!tlmi_priv.pwd_admin->signature || - !tlmi_priv.pwd_admin->save_signature) { - ret = -EINVAL; - goto out; - } - ret = tlmi_simple_call(LENOVO_SAVE_BIOS_SETTING_CERT_GUID, - tlmi_priv.pwd_admin->save_signature); - if (ret) - goto out; - } else if (tlmi_priv.opcode_support) { - if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { - ret = tlmi_opcode_setting("WmiOpcodePasswordAdmin", - tlmi_priv.pwd_admin->password); - if (ret) - goto out; - } - ret = tlmi_save_bios_settings(""); - } else { /* old non-opcode based authentication method (deprecated) */ - if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { - auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", - tlmi_priv.pwd_admin->password, - encoding_options[tlmi_priv.pwd_admin->encoding], - tlmi_priv.pwd_admin->kbdlang); - if (!auth_str) { - ret = -ENOMEM; - goto out; - } - } - - if (auth_str) - ret = tlmi_save_bios_settings(auth_str); - else - ret = tlmi_save_bios_settings(""); - } - tlmi_priv.save_required = false; - tlmi_priv.reboot_required = true; - - if (!ret && !tlmi_priv.pending_changes) { - tlmi_priv.pending_changes = true; - /* let userland know it may need to check reboot pending again */ - kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); - } - break; - } -out: - mutex_unlock(&tlmi_mutex); - kfree(auth_str); - return ret ?: count; -} - -static struct kobj_attribute save_settings = __ATTR_RW(save_settings); - -/* ---- Debug interface--------------------------------------------------------- */ -static ssize_t debug_cmd_store(struct kobject *kobj, struct kobj_attribute *attr, - const char *buf, size_t count) -{ - char *set_str = NULL, *new_setting = NULL; - char *auth_str = NULL; - int ret; - - if (!tlmi_priv.can_debug_cmd) - return -EOPNOTSUPP; - - /* Strip out CR if one is present */ - new_setting = kstrdup_and_replace(buf, '\n', '\0', GFP_KERNEL); - if (!new_setting) - return -ENOMEM; - - if (tlmi_priv.pwd_admin->pwd_enabled && tlmi_priv.pwd_admin->password[0]) { - auth_str = kasprintf(GFP_KERNEL, "%s,%s,%s;", - tlmi_priv.pwd_admin->password, - encoding_options[tlmi_priv.pwd_admin->encoding], - tlmi_priv.pwd_admin->kbdlang); - if (!auth_str) { - ret = -ENOMEM; - goto out; - } - } - - if (auth_str) - set_str = kasprintf(GFP_KERNEL, "%s,%s", new_setting, auth_str); - else - set_str = kasprintf(GFP_KERNEL, "%s;", new_setting); - if (!set_str) { - ret = -ENOMEM; - goto out; - } - - ret = tlmi_simple_call(LENOVO_DEBUG_CMD_GUID, set_str); - if (ret) - goto out; - - if (!ret && !tlmi_priv.pending_changes) { - tlmi_priv.pending_changes = true; - /* let userland know it may need to check reboot pending again */ - kobject_uevent(&tlmi_priv.class_dev->kobj, KOBJ_CHANGE); - } -out: - kfree(auth_str); - kfree(set_str); - kfree(new_setting); - return ret ?: count; -} - -static struct kobj_attribute debug_cmd = __ATTR_WO(debug_cmd); - -/* ---- Initialisation --------------------------------------------------------- */ -static void tlmi_release_attr(void) -{ - int i; - - /* Attribute structures */ - for (i = 0; i < TLMI_SETTINGS_COUNT; i++) { - if (tlmi_priv.setting[i]) { - sysfs_remove_group(&tlmi_priv.setting[i]->kobj, &tlmi_attr_group); - kobject_put(&tlmi_priv.setting[i]->kobj); - } - } - sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); - sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); - - if (tlmi_priv.can_debug_cmd && debug_support) - sysfs_remove_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); - - kset_unregister(tlmi_priv.attribute_kset); - - /* Free up any saved signatures */ - kfree(tlmi_priv.pwd_admin->signature); - kfree(tlmi_priv.pwd_admin->save_signature); - - /* Authentication structures */ - sysfs_remove_group(&tlmi_priv.pwd_admin->kobj, &auth_attr_group); - kobject_put(&tlmi_priv.pwd_admin->kobj); - sysfs_remove_group(&tlmi_priv.pwd_power->kobj, &auth_attr_group); - kobject_put(&tlmi_priv.pwd_power->kobj); - - if (tlmi_priv.opcode_support) { - sysfs_remove_group(&tlmi_priv.pwd_system->kobj, &auth_attr_group); - kobject_put(&tlmi_priv.pwd_system->kobj); - sysfs_remove_group(&tlmi_priv.pwd_hdd->kobj, &auth_attr_group); - kobject_put(&tlmi_priv.pwd_hdd->kobj); - sysfs_remove_group(&tlmi_priv.pwd_nvme->kobj, &auth_attr_group); - kobject_put(&tlmi_priv.pwd_nvme->kobj); - } - - kset_unregister(tlmi_priv.authentication_kset); -} - -static int tlmi_validate_setting_name(struct kset *attribute_kset, char *name) -{ - struct kobject *duplicate; - - if (!strcmp(name, "Reserved")) - return -EINVAL; - - duplicate = kset_find_obj(attribute_kset, name); - if (duplicate) { - pr_debug("Duplicate attribute name found - %s\n", name); - /* kset_find_obj() returns a reference */ - kobject_put(duplicate); - return -EBUSY; - } - - return 0; -} - -static int tlmi_sysfs_init(void) -{ - int i, ret; - - tlmi_priv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), - NULL, "%s", "thinklmi"); - if (IS_ERR(tlmi_priv.class_dev)) { - ret = PTR_ERR(tlmi_priv.class_dev); - goto fail_class_created; - } - - tlmi_priv.attribute_kset = kset_create_and_add("attributes", NULL, - &tlmi_priv.class_dev->kobj); - if (!tlmi_priv.attribute_kset) { - ret = -ENOMEM; - goto fail_device_created; - } - - for (i = 0; i < TLMI_SETTINGS_COUNT; i++) { - /* Check if index is a valid setting - skip if it isn't */ - if (!tlmi_priv.setting[i]) - continue; - - /* check for duplicate or reserved values */ - if (tlmi_validate_setting_name(tlmi_priv.attribute_kset, - tlmi_priv.setting[i]->display_name) < 0) { - kfree(tlmi_priv.setting[i]->possible_values); - kfree(tlmi_priv.setting[i]); - tlmi_priv.setting[i] = NULL; - continue; - } - - /* Build attribute */ - tlmi_priv.setting[i]->kobj.kset = tlmi_priv.attribute_kset; - ret = kobject_add(&tlmi_priv.setting[i]->kobj, NULL, - "%s", tlmi_priv.setting[i]->display_name); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.setting[i]->kobj, &tlmi_attr_group); - if (ret) - goto fail_create_attr; - } - - ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &pending_reboot.attr); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &save_settings.attr); - if (ret) - goto fail_create_attr; - - if (tlmi_priv.can_debug_cmd && debug_support) { - ret = sysfs_create_file(&tlmi_priv.attribute_kset->kobj, &debug_cmd.attr); - if (ret) - goto fail_create_attr; - } - - /* Create authentication entries */ - tlmi_priv.authentication_kset = kset_create_and_add("authentication", NULL, - &tlmi_priv.class_dev->kobj); - if (!tlmi_priv.authentication_kset) { - ret = -ENOMEM; - goto fail_create_attr; - } - tlmi_priv.pwd_admin->kobj.kset = tlmi_priv.authentication_kset; - ret = kobject_add(&tlmi_priv.pwd_admin->kobj, NULL, "%s", "Admin"); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.pwd_admin->kobj, &auth_attr_group); - if (ret) - goto fail_create_attr; - - tlmi_priv.pwd_power->kobj.kset = tlmi_priv.authentication_kset; - ret = kobject_add(&tlmi_priv.pwd_power->kobj, NULL, "%s", "Power-on"); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.pwd_power->kobj, &auth_attr_group); - if (ret) - goto fail_create_attr; - - if (tlmi_priv.opcode_support) { - tlmi_priv.pwd_system->kobj.kset = tlmi_priv.authentication_kset; - ret = kobject_add(&tlmi_priv.pwd_system->kobj, NULL, "%s", "System"); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.pwd_system->kobj, &auth_attr_group); - if (ret) - goto fail_create_attr; - - tlmi_priv.pwd_hdd->kobj.kset = tlmi_priv.authentication_kset; - ret = kobject_add(&tlmi_priv.pwd_hdd->kobj, NULL, "%s", "HDD"); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.pwd_hdd->kobj, &auth_attr_group); - if (ret) - goto fail_create_attr; - - tlmi_priv.pwd_nvme->kobj.kset = tlmi_priv.authentication_kset; - ret = kobject_add(&tlmi_priv.pwd_nvme->kobj, NULL, "%s", "NVMe"); - if (ret) - goto fail_create_attr; - - ret = sysfs_create_group(&tlmi_priv.pwd_nvme->kobj, &auth_attr_group); - if (ret) - goto fail_create_attr; - } - - return ret; - -fail_create_attr: - tlmi_release_attr(); -fail_device_created: - device_destroy(&firmware_attributes_class, MKDEV(0, 0)); -fail_class_created: - return ret; -} - -/* ---- Base Driver -------------------------------------------------------- */ -static struct tlmi_pwd_setting *tlmi_create_auth(const char *pwd_type, - const char *pwd_role) -{ - struct tlmi_pwd_setting *new_pwd; - - new_pwd = kzalloc(sizeof(struct tlmi_pwd_setting), GFP_KERNEL); - if (!new_pwd) - return NULL; - - strscpy(new_pwd->kbdlang, "us"); - new_pwd->encoding = TLMI_ENCODING_ASCII; - new_pwd->pwd_type = pwd_type; - new_pwd->role = pwd_role; - new_pwd->minlen = tlmi_priv.pwdcfg.core.min_length; - new_pwd->maxlen = tlmi_priv.pwdcfg.core.max_length; - new_pwd->index = 0; - - kobject_init(&new_pwd->kobj, &tlmi_pwd_setting_ktype); - - return new_pwd; -} - -static int tlmi_analyze(struct wmi_device *wdev) -{ - int i, ret; - - if (wmi_has_guid(LENOVO_SET_BIOS_SETTINGS_GUID) && - wmi_has_guid(LENOVO_SAVE_BIOS_SETTINGS_GUID)) - tlmi_priv.can_set_bios_settings = true; - - if (wmi_has_guid(LENOVO_GET_BIOS_SELECTIONS_GUID)) - tlmi_priv.can_get_bios_selections = true; - - if (wmi_has_guid(LENOVO_SET_BIOS_PASSWORD_GUID)) - tlmi_priv.can_set_bios_password = true; - - if (wmi_has_guid(LENOVO_BIOS_PASSWORD_SETTINGS_GUID)) - tlmi_priv.can_get_password_settings = true; - - if (wmi_has_guid(LENOVO_DEBUG_CMD_GUID)) - tlmi_priv.can_debug_cmd = true; - - if (wmi_has_guid(LENOVO_OPCODE_IF_GUID)) - tlmi_priv.opcode_support = true; - - if (wmi_has_guid(LENOVO_SET_BIOS_CERT_GUID) && - wmi_has_guid(LENOVO_SET_BIOS_SETTING_CERT_GUID) && - wmi_has_guid(LENOVO_SAVE_BIOS_SETTING_CERT_GUID)) - tlmi_priv.certificate_support = true; - - /* - * Try to find the number of valid settings of this machine - * and use it to create sysfs attributes. - */ - for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { - struct tlmi_attr_setting *setting; - char *item = NULL; - - tlmi_priv.setting[i] = NULL; - ret = tlmi_setting(wdev, i, &item); - if (ret) - break; - if (!item) - break; - if (!*item) { - kfree(item); - continue; - } - - /* Remove the value part */ - strreplace(item, ',', '\0'); - - /* Create a setting entry */ - setting = kzalloc(sizeof(*setting), GFP_KERNEL); - if (!setting) { - ret = -ENOMEM; - kfree(item); - goto fail_clear_attr; - } - setting->wdev = wdev; - setting->index = i; - - strscpy(setting->name, item); - /* It is not allowed to have '/' for file name. Convert it into '\'. */ - strreplace(item, '/', '\\'); - strscpy(setting->display_name, item); - - /* If BIOS selections supported, load those */ - if (tlmi_priv.can_get_bios_selections) { - ret = tlmi_get_bios_selections(setting->name, - &setting->possible_values); - if (ret || !setting->possible_values) - pr_info("Error retrieving possible values for %d : %s\n", - i, setting->display_name); - } else { - /* - * Older Thinkstations don't support the bios_selections API. - * Instead they store this as a [Optional:Option1,Option2] section of the - * name string. - * Try and pull that out if it's available. - */ - char *optitem, *optstart, *optend; - - if (!tlmi_setting(setting->wdev, setting->index, &optitem)) { - optstart = strstr(optitem, "[Optional:"); - if (optstart) { - optstart += strlen("[Optional:"); - optend = strstr(optstart, "]"); - if (optend) - setting->possible_values = - kstrndup(optstart, optend - optstart, - GFP_KERNEL); - } - kfree(optitem); - } - } - /* - * firmware-attributes requires that possible_values are separated by ';' but - * Lenovo FW uses ','. Replace appropriately. - */ - if (setting->possible_values) - strreplace(setting->possible_values, ',', ';'); - - kobject_init(&setting->kobj, &tlmi_attr_setting_ktype); - tlmi_priv.setting[i] = setting; - kfree(item); - } - - /* Create password setting structure */ - ret = tlmi_get_pwd_settings(&tlmi_priv.pwdcfg); - if (ret) - goto fail_clear_attr; - - /* All failures below boil down to kmalloc failures */ - ret = -ENOMEM; - - tlmi_priv.pwd_admin = tlmi_create_auth("pap", "bios-admin"); - if (!tlmi_priv.pwd_admin) - goto fail_clear_attr; - - if (tlmi_priv.pwdcfg.core.password_state & TLMI_PAP_PWD) - tlmi_priv.pwd_admin->pwd_enabled = true; - - tlmi_priv.pwd_power = tlmi_create_auth("pop", "power-on"); - if (!tlmi_priv.pwd_power) - goto fail_clear_attr; - - if (tlmi_priv.pwdcfg.core.password_state & TLMI_POP_PWD) - tlmi_priv.pwd_power->pwd_enabled = true; - - if (tlmi_priv.opcode_support) { - tlmi_priv.pwd_system = tlmi_create_auth("smp", "system"); - if (!tlmi_priv.pwd_system) - goto fail_clear_attr; - - if (tlmi_priv.pwdcfg.core.password_state & TLMI_SMP_PWD) - tlmi_priv.pwd_system->pwd_enabled = true; - - tlmi_priv.pwd_hdd = tlmi_create_auth("hdd", "hdd"); - if (!tlmi_priv.pwd_hdd) - goto fail_clear_attr; - - tlmi_priv.pwd_nvme = tlmi_create_auth("nvm", "nvme"); - if (!tlmi_priv.pwd_nvme) - goto fail_clear_attr; - - /* Set default hdd/nvme index to 1 as there is no device 0 */ - tlmi_priv.pwd_hdd->index = 1; - tlmi_priv.pwd_nvme->index = 1; - - if (tlmi_priv.pwdcfg.core.password_state & TLMI_HDD_PWD) { - /* Check if PWD is configured and set index to first drive found */ - if (tlmi_priv.pwdcfg.ext.hdd_user_password || - tlmi_priv.pwdcfg.ext.hdd_master_password) { - tlmi_priv.pwd_hdd->pwd_enabled = true; - if (tlmi_priv.pwdcfg.ext.hdd_master_password) - tlmi_priv.pwd_hdd->index = - ffs(tlmi_priv.pwdcfg.ext.hdd_master_password) - 1; - else - tlmi_priv.pwd_hdd->index = - ffs(tlmi_priv.pwdcfg.ext.hdd_user_password) - 1; - } - if (tlmi_priv.pwdcfg.ext.nvme_user_password || - tlmi_priv.pwdcfg.ext.nvme_master_password) { - tlmi_priv.pwd_nvme->pwd_enabled = true; - if (tlmi_priv.pwdcfg.ext.nvme_master_password) - tlmi_priv.pwd_nvme->index = - ffs(tlmi_priv.pwdcfg.ext.nvme_master_password) - 1; - else - tlmi_priv.pwd_nvme->index = - ffs(tlmi_priv.pwdcfg.ext.nvme_user_password) - 1; - } - } - } - - if (tlmi_priv.certificate_support) { - tlmi_priv.pwd_admin->cert_installed = - tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SVC; - tlmi_priv.pwd_system->cert_installed = - tlmi_priv.pwdcfg.core.password_state & TLMI_CERT_SMC; - } - return 0; - -fail_clear_attr: - for (i = 0; i < TLMI_SETTINGS_COUNT; ++i) { - if (tlmi_priv.setting[i]) { - kfree(tlmi_priv.setting[i]->possible_values); - kfree(tlmi_priv.setting[i]); - } - } - kfree(tlmi_priv.pwd_admin); - kfree(tlmi_priv.pwd_power); - kfree(tlmi_priv.pwd_system); - kfree(tlmi_priv.pwd_hdd); - kfree(tlmi_priv.pwd_nvme); - return ret; -} - -static void tlmi_remove(struct wmi_device *wdev) -{ - tlmi_release_attr(); - device_destroy(&firmware_attributes_class, MKDEV(0, 0)); -} - -static int tlmi_probe(struct wmi_device *wdev, const void *context) -{ - int ret; - - ret = tlmi_analyze(wdev); - if (ret) - return ret; - - return tlmi_sysfs_init(); -} - -static const struct wmi_device_id tlmi_id_table[] = { - { .guid_string = LENOVO_BIOS_SETTING_GUID }, - { } -}; -MODULE_DEVICE_TABLE(wmi, tlmi_id_table); - -static struct wmi_driver tlmi_driver = { - .driver = { - .name = "think-lmi", - }, - .id_table = tlmi_id_table, - .probe = tlmi_probe, - .remove = tlmi_remove, -}; - -MODULE_AUTHOR("Sugumaran L "); -MODULE_AUTHOR("Mark Pearson "); -MODULE_AUTHOR("Corentin Chary "); -MODULE_DESCRIPTION("ThinkLMI Driver"); -MODULE_LICENSE("GPL"); - -module_wmi_driver(tlmi_driver); diff --git a/drivers/platform/x86/think-lmi.h b/drivers/platform/x86/think-lmi.h deleted file mode 100644 index 9b014644d316..000000000000 --- a/drivers/platform/x86/think-lmi.h +++ /dev/null @@ -1,126 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -#ifndef _THINK_LMI_H_ -#define _THINK_LMI_H_ - -#include -#include - -#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, -}; - -/* 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; - - 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; -}; - -#endif /* !_THINK_LMI_H_ */ diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c deleted file mode 100644 index e7350c9fa3aa..000000000000 --- a/drivers/platform/x86/thinkpad_acpi.c +++ /dev/null @@ -1,12096 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * thinkpad_acpi.c - ThinkPad ACPI Extras - * - * Copyright (C) 2004-2005 Borislav Deianov - * Copyright (C) 2006-2009 Henrique de Moraes Holschuh - */ - -#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt - -#define TPACPI_VERSION "0.26" -#define TPACPI_SYSFS_VERSION 0x030000 - -/* - * Changelog: - * 2007-10-20 changelog trimmed down - * - * 2007-03-27 0.14 renamed to thinkpad_acpi and moved to - * drivers/misc. - * - * 2006-11-22 0.13 new maintainer - * changelog now lives in git commit history, and will - * not be updated further in-file. - * - * 2005-03-17 0.11 support for 600e, 770x - * thanks to Jamie Lentin - * - * 2005-01-16 0.9 use MODULE_VERSION - * thanks to Henrik Brix Andersen - * fix parameter passing on module loading - * thanks to Rusty Russell - * thanks to Jim Radford - * 2004-11-08 0.8 fix init error case, don't return from a macro - * thanks to Chris Wright - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - -#include "dual_accel_detect.h" - -/* ThinkPad CMOS commands */ -#define TP_CMOS_VOLUME_DOWN 0 -#define TP_CMOS_VOLUME_UP 1 -#define TP_CMOS_VOLUME_MUTE 2 -#define TP_CMOS_BRIGHTNESS_UP 4 -#define TP_CMOS_BRIGHTNESS_DOWN 5 -#define TP_CMOS_THINKLIGHT_ON 12 -#define TP_CMOS_THINKLIGHT_OFF 13 - -/* NVRAM Addresses */ -enum tp_nvram_addr { - TP_NVRAM_ADDR_HK2 = 0x57, - TP_NVRAM_ADDR_THINKLIGHT = 0x58, - TP_NVRAM_ADDR_VIDEO = 0x59, - TP_NVRAM_ADDR_BRIGHTNESS = 0x5e, - TP_NVRAM_ADDR_MIXER = 0x60, -}; - -/* NVRAM bit masks */ -enum { - TP_NVRAM_MASK_HKT_THINKPAD = 0x08, - TP_NVRAM_MASK_HKT_ZOOM = 0x20, - TP_NVRAM_MASK_HKT_DISPLAY = 0x40, - TP_NVRAM_MASK_HKT_HIBERNATE = 0x80, - TP_NVRAM_MASK_THINKLIGHT = 0x10, - TP_NVRAM_MASK_HKT_DISPEXPND = 0x30, - TP_NVRAM_MASK_HKT_BRIGHTNESS = 0x20, - TP_NVRAM_MASK_LEVEL_BRIGHTNESS = 0x0f, - TP_NVRAM_POS_LEVEL_BRIGHTNESS = 0, - TP_NVRAM_MASK_MUTE = 0x40, - TP_NVRAM_MASK_HKT_VOLUME = 0x80, - TP_NVRAM_MASK_LEVEL_VOLUME = 0x0f, - TP_NVRAM_POS_LEVEL_VOLUME = 0, -}; - -/* Misc NVRAM-related */ -enum { - TP_NVRAM_LEVEL_VOLUME_MAX = 14, -}; - -/* ACPI HIDs */ -#define TPACPI_ACPI_IBM_HKEY_HID "IBM0068" -#define TPACPI_ACPI_LENOVO_HKEY_HID "LEN0068" -#define TPACPI_ACPI_LENOVO_HKEY_V2_HID "LEN0268" -#define TPACPI_ACPI_EC_HID "PNP0C09" - -/* Input IDs */ -#define TPACPI_HKEY_INPUT_PRODUCT 0x5054 /* "TP" */ -#define TPACPI_HKEY_INPUT_VERSION 0x4101 - -/* ACPI \WGSV commands */ -enum { - TP_ACPI_WGSV_GET_STATE = 0x01, /* Get state information */ - TP_ACPI_WGSV_PWR_ON_ON_RESUME = 0x02, /* Resume WWAN powered on */ - TP_ACPI_WGSV_PWR_OFF_ON_RESUME = 0x03, /* Resume WWAN powered off */ - TP_ACPI_WGSV_SAVE_STATE = 0x04, /* Save state for S4/S5 */ -}; - -/* TP_ACPI_WGSV_GET_STATE bits */ -enum { - TP_ACPI_WGSV_STATE_WWANEXIST = 0x0001, /* WWAN hw available */ - TP_ACPI_WGSV_STATE_WWANPWR = 0x0002, /* WWAN radio enabled */ - TP_ACPI_WGSV_STATE_WWANPWRRES = 0x0004, /* WWAN state at resume */ - TP_ACPI_WGSV_STATE_WWANBIOSOFF = 0x0008, /* WWAN disabled in BIOS */ - TP_ACPI_WGSV_STATE_BLTHEXIST = 0x0001, /* BLTH hw available */ - TP_ACPI_WGSV_STATE_BLTHPWR = 0x0002, /* BLTH radio enabled */ - TP_ACPI_WGSV_STATE_BLTHPWRRES = 0x0004, /* BLTH state at resume */ - TP_ACPI_WGSV_STATE_BLTHBIOSOFF = 0x0008, /* BLTH disabled in BIOS */ - TP_ACPI_WGSV_STATE_UWBEXIST = 0x0010, /* UWB hw available */ - TP_ACPI_WGSV_STATE_UWBPWR = 0x0020, /* UWB radio enabled */ -}; - -/* HKEY events */ -enum tpacpi_hkey_event_t { - /* Original hotkeys */ - TP_HKEY_EV_ORIG_KEY_START = 0x1001, /* First hotkey (FN+F1) */ - TP_HKEY_EV_BRGHT_UP = 0x1010, /* Brightness up */ - TP_HKEY_EV_BRGHT_DOWN = 0x1011, /* Brightness down */ - TP_HKEY_EV_KBD_LIGHT = 0x1012, /* Thinklight/kbd backlight */ - TP_HKEY_EV_VOL_UP = 0x1015, /* Volume up or unmute */ - TP_HKEY_EV_VOL_DOWN = 0x1016, /* Volume down or unmute */ - TP_HKEY_EV_VOL_MUTE = 0x1017, /* Mixer output mute */ - TP_HKEY_EV_ORIG_KEY_END = 0x1020, /* Last original hotkey code */ - - /* Adaptive keyboard (2014 X1 Carbon) */ - TP_HKEY_EV_DFR_CHANGE_ROW = 0x1101, /* Change adaptive kbd Fn row mode */ - TP_HKEY_EV_DFR_S_QUICKVIEW_ROW = 0x1102, /* Set adap. kbd Fn row to function mode */ - TP_HKEY_EV_ADAPTIVE_KEY_START = 0x1103, /* First hotkey code on adaptive kbd */ - TP_HKEY_EV_ADAPTIVE_KEY_END = 0x1116, /* Last hotkey code on adaptive kbd */ - - /* Extended hotkey events in 2017+ models */ - TP_HKEY_EV_EXTENDED_KEY_START = 0x1300, /* First extended hotkey code */ - TP_HKEY_EV_PRIVACYGUARD_TOGGLE = 0x130f, /* Toggle priv.guard on/off */ - TP_HKEY_EV_EXTENDED_KEY_END = 0x1319, /* Last extended hotkey code using - * hkey -> scancode translation for - * compat. Later codes are entered - * directly in the sparse-keymap. - */ - TP_HKEY_EV_AMT_TOGGLE = 0x131a, /* Toggle AMT on/off */ - TP_HKEY_EV_CAMERASHUTTER_TOGGLE = 0x131b, /* Toggle Camera Shutter */ - TP_HKEY_EV_DOUBLETAP_TOGGLE = 0x131c, /* Toggle trackpoint doubletap on/off */ - TP_HKEY_EV_PROFILE_TOGGLE = 0x131f, /* Toggle platform profile in 2024 systems */ - TP_HKEY_EV_PROFILE_TOGGLE2 = 0x1401, /* Toggle platform profile in 2025 + systems */ - - /* Reasons for waking up from S3/S4 */ - TP_HKEY_EV_WKUP_S3_UNDOCK = 0x2304, /* undock requested, S3 */ - TP_HKEY_EV_WKUP_S4_UNDOCK = 0x2404, /* undock requested, S4 */ - TP_HKEY_EV_WKUP_S3_BAYEJ = 0x2305, /* bay ejection req, S3 */ - TP_HKEY_EV_WKUP_S4_BAYEJ = 0x2405, /* bay ejection req, S4 */ - TP_HKEY_EV_WKUP_S3_BATLOW = 0x2313, /* battery empty, S3 */ - TP_HKEY_EV_WKUP_S4_BATLOW = 0x2413, /* battery empty, S4 */ - - /* Auto-sleep after eject request */ - TP_HKEY_EV_BAYEJ_ACK = 0x3003, /* bay ejection complete */ - TP_HKEY_EV_UNDOCK_ACK = 0x4003, /* undock complete */ - - /* Misc bay events */ - TP_HKEY_EV_OPTDRV_EJ = 0x3006, /* opt. drive tray ejected */ - TP_HKEY_EV_HOTPLUG_DOCK = 0x4010, /* docked into hotplug dock - or port replicator */ - TP_HKEY_EV_HOTPLUG_UNDOCK = 0x4011, /* undocked from hotplug - dock or port replicator */ - /* - * Thinkpad X1 Tablet series devices emit 0x4012 and 0x4013 - * when keyboard cover is attached, detached or folded onto the back - */ - TP_HKEY_EV_KBD_COVER_ATTACH = 0x4012, /* keyboard cover attached */ - TP_HKEY_EV_KBD_COVER_DETACH = 0x4013, /* keyboard cover detached or folded back */ - - /* User-interface events */ - TP_HKEY_EV_LID_CLOSE = 0x5001, /* laptop lid closed */ - TP_HKEY_EV_LID_OPEN = 0x5002, /* laptop lid opened */ - TP_HKEY_EV_TABLET_TABLET = 0x5009, /* tablet swivel up */ - TP_HKEY_EV_TABLET_NOTEBOOK = 0x500a, /* tablet swivel down */ - TP_HKEY_EV_TABLET_CHANGED = 0x60c0, /* X1 Yoga (2016): - * enter/leave tablet mode - */ - TP_HKEY_EV_PEN_INSERTED = 0x500b, /* tablet pen inserted */ - TP_HKEY_EV_PEN_REMOVED = 0x500c, /* tablet pen removed */ - TP_HKEY_EV_BRGHT_CHANGED = 0x5010, /* backlight control event */ - - /* Key-related user-interface events */ - TP_HKEY_EV_KEY_NUMLOCK = 0x6000, /* NumLock key pressed */ - TP_HKEY_EV_KEY_FN = 0x6005, /* Fn key pressed? E420 */ - TP_HKEY_EV_KEY_FN_ESC = 0x6060, /* Fn+Esc key pressed X240 */ - - /* Thermal events */ - TP_HKEY_EV_ALARM_BAT_HOT = 0x6011, /* battery too hot */ - TP_HKEY_EV_ALARM_BAT_XHOT = 0x6012, /* battery critically hot */ - TP_HKEY_EV_ALARM_BAT_LIM_CHANGE = 0x6013, /* battery charge limit changed*/ - TP_HKEY_EV_ALARM_SENSOR_HOT = 0x6021, /* sensor too hot */ - TP_HKEY_EV_ALARM_SENSOR_XHOT = 0x6022, /* sensor critically hot */ - TP_HKEY_EV_THM_TABLE_CHANGED = 0x6030, /* windows; thermal table changed */ - TP_HKEY_EV_THM_CSM_COMPLETED = 0x6032, /* windows; thermal control set - * command completed. Related to - * AML DYTC */ - TP_HKEY_EV_THM_TRANSFM_CHANGED = 0x60F0, /* windows; thermal transformation - * changed. Related to AML GMTS */ - - /* AC-related events */ - TP_HKEY_EV_AC_CHANGED = 0x6040, /* AC status changed */ - - /* Further user-interface events */ - TP_HKEY_EV_PALM_DETECTED = 0x60b0, /* palm hoveres keyboard */ - TP_HKEY_EV_PALM_UNDETECTED = 0x60b1, /* palm removed */ - - /* Misc */ - TP_HKEY_EV_RFKILL_CHANGED = 0x7000, /* rfkill switch changed */ - - /* Misc2 */ - TP_HKEY_EV_TRACK_DOUBLETAP = 0x8036, /* trackpoint doubletap */ -}; - -/**************************************************************************** - * Main driver - */ - -#define TPACPI_NAME "thinkpad" -#define TPACPI_DESC "ThinkPad ACPI Extras" -#define TPACPI_FILE TPACPI_NAME "_acpi" -#define TPACPI_URL "http://ibm-acpi.sf.net/" -#define TPACPI_MAIL "ibm-acpi-devel@lists.sourceforge.net" - -#define TPACPI_PROC_DIR "ibm" -#define TPACPI_ACPI_EVENT_PREFIX "ibm" -#define TPACPI_DRVR_NAME TPACPI_FILE -#define TPACPI_DRVR_SHORTNAME "tpacpi" -#define TPACPI_HWMON_DRVR_NAME TPACPI_NAME "_hwmon" - -#define TPACPI_NVRAM_KTHREAD_NAME "ktpacpi_nvramd" -#define TPACPI_WORKQUEUE_NAME "ktpacpid" - -#define TPACPI_MAX_ACPI_ARGS 3 - -/* Debugging printk groups */ -#define TPACPI_DBG_ALL 0xffff -#define TPACPI_DBG_DISCLOSETASK 0x8000 -#define TPACPI_DBG_INIT 0x0001 -#define TPACPI_DBG_EXIT 0x0002 -#define TPACPI_DBG_RFKILL 0x0004 -#define TPACPI_DBG_HKEY 0x0008 -#define TPACPI_DBG_FAN 0x0010 -#define TPACPI_DBG_BRGHT 0x0020 -#define TPACPI_DBG_MIXER 0x0040 - -#define FAN_NOT_PRESENT 65535 - -/**************************************************************************** - * Driver-wide structs and misc. variables - */ - -struct ibm_struct; - -struct tp_acpi_drv_struct { - const struct acpi_device_id *hid; - struct acpi_driver *driver; - - void (*notify) (struct ibm_struct *, u32); - acpi_handle *handle; - u32 type; - struct acpi_device *device; -}; - -struct ibm_struct { - char *name; - - int (*read) (struct seq_file *); - int (*write) (char *); - void (*exit) (void); - void (*resume) (void); - void (*suspend) (void); - void (*shutdown) (void); - - struct list_head all_drivers; - - struct tp_acpi_drv_struct *acpi; - - struct { - u8 acpi_driver_registered:1; - u8 acpi_notify_installed:1; - u8 proc_created:1; - u8 init_called:1; - u8 experimental:1; - } flags; -}; - -struct ibm_init_struct { - char param[32]; - - int (*init) (struct ibm_init_struct *); - umode_t base_procfs_mode; - struct ibm_struct *data; -}; - -/* DMI Quirks */ -struct quirk_entry { - bool btusb_bug; -}; - -static struct quirk_entry quirk_btusb_bug = { - .btusb_bug = true, -}; - -static struct { - u32 bluetooth:1; - u32 hotkey:1; - u32 hotkey_mask:1; - u32 hotkey_wlsw:1; - enum { - TP_HOTKEY_TABLET_NONE = 0, - TP_HOTKEY_TABLET_USES_MHKG, - TP_HOTKEY_TABLET_USES_GMMS, - } hotkey_tablet; - u32 kbdlight:1; - u32 light:1; - u32 light_status:1; - u32 bright_acpimode:1; - u32 bright_unkfw:1; - u32 wan:1; - u32 uwb:1; - u32 fan_ctrl_status_undef:1; - u32 second_fan:1; - u32 second_fan_ctl:1; - u32 beep_needs_two_args:1; - u32 mixer_no_level_control:1; - u32 battery_force_primary:1; - u32 platform_drv_registered:1; - u32 hotkey_poll_active:1; - u32 has_adaptive_kbd:1; - u32 kbd_lang:1; - u32 trackpoint_doubletap:1; - struct quirk_entry *quirks; -} tp_features; - -static struct { - u16 hotkey_mask_ff:1; - u16 volume_ctrl_forbidden:1; -} tp_warned; - -struct thinkpad_id_data { - unsigned int vendor; /* ThinkPad vendor: - * PCI_VENDOR_ID_IBM/PCI_VENDOR_ID_LENOVO */ - - char *bios_version_str; /* Something like 1ZET51WW (1.03z) */ - char *ec_version_str; /* Something like 1ZHT51WW-1.04a */ - - u32 bios_model; /* 1Y = 0x3159, 0 = unknown */ - u32 ec_model; - u16 bios_release; /* 1ZETK1WW = 0x4b31, 0 = unknown */ - u16 ec_release; - - char *model_str; /* ThinkPad T43 */ - char *nummodel_str; /* 9384A9C for a 9384-A9C model */ -}; -static struct thinkpad_id_data thinkpad_id; - -static enum { - TPACPI_LIFE_INIT = 0, - TPACPI_LIFE_RUNNING, - TPACPI_LIFE_EXITING, -} tpacpi_lifecycle; - -static int experimental; -static u32 dbg_level; - -static struct workqueue_struct *tpacpi_wq; - -enum led_status_t { - TPACPI_LED_OFF = 0, - TPACPI_LED_ON, - TPACPI_LED_BLINK, -}; - -/* tpacpi LED class */ -struct tpacpi_led_classdev { - struct led_classdev led_classdev; - int led; -}; - -/* brightness level capabilities */ -static unsigned int bright_maxlvl; /* 0 = unknown */ - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES -static int dbg_wlswemul; -static bool tpacpi_wlsw_emulstate; -static int dbg_bluetoothemul; -static bool tpacpi_bluetooth_emulstate; -static int dbg_wwanemul; -static bool tpacpi_wwan_emulstate; -static int dbg_uwbemul; -static bool tpacpi_uwb_emulstate; -#endif - - -/************************************************************************* - * Debugging helpers - */ - -#define dbg_printk(a_dbg_level, format, arg...) \ -do { \ - if (dbg_level & (a_dbg_level)) \ - printk(KERN_DEBUG pr_fmt("%s: " format), \ - __func__, ##arg); \ -} while (0) - -#ifdef CONFIG_THINKPAD_ACPI_DEBUG -#define vdbg_printk dbg_printk -static const char *str_supported(int is_supported); -#else -static inline const char *str_supported(int is_supported) { return ""; } -#define vdbg_printk(a_dbg_level, format, arg...) \ - do { if (0) no_printk(format, ##arg); } while (0) -#endif - -static void tpacpi_log_usertask(const char * const what) -{ - printk(KERN_DEBUG pr_fmt("%s: access by process with PID %d\n"), - what, task_tgid_vnr(current)); -} - -#define tpacpi_disclose_usertask(what, format, arg...) \ -do { \ - if (unlikely((dbg_level & TPACPI_DBG_DISCLOSETASK) && \ - (tpacpi_lifecycle == TPACPI_LIFE_RUNNING))) { \ - printk(KERN_DEBUG pr_fmt("%s: PID %d: " format), \ - what, task_tgid_vnr(current), ## arg); \ - } \ -} while (0) - -/* - * Quirk handling helpers - * - * ThinkPad IDs and versions seen in the field so far are - * two or three characters from the set [0-9A-Z], i.e. base 36. - * - * We use values well outside that range as specials. - */ - -#define TPACPI_MATCH_ANY 0xffffffffU -#define TPACPI_MATCH_ANY_VERSION 0xffffU -#define TPACPI_MATCH_UNKNOWN 0U - -/* TPID('1', 'Y') == 0x3159 */ -#define TPID(__c1, __c2) (((__c1) << 8) | (__c2)) -#define TPID3(__c1, __c2, __c3) (((__c1) << 16) | ((__c2) << 8) | (__c3)) -#define TPVER TPID - -#define TPACPI_Q_IBM(__id1, __id2, __quirk) \ - { .vendor = PCI_VENDOR_ID_IBM, \ - .bios = TPID(__id1, __id2), \ - .ec = TPACPI_MATCH_ANY, \ - .quirks = (__quirk) } - -#define TPACPI_Q_LNV(__id1, __id2, __quirk) \ - { .vendor = PCI_VENDOR_ID_LENOVO, \ - .bios = TPID(__id1, __id2), \ - .ec = TPACPI_MATCH_ANY, \ - .quirks = (__quirk) } - -#define TPACPI_Q_LNV3(__id1, __id2, __id3, __quirk) \ - { .vendor = PCI_VENDOR_ID_LENOVO, \ - .bios = TPID3(__id1, __id2, __id3), \ - .ec = TPACPI_MATCH_ANY, \ - .quirks = (__quirk) } - -#define TPACPI_QEC_IBM(__id1, __id2, __quirk) \ - { .vendor = PCI_VENDOR_ID_IBM, \ - .bios = TPACPI_MATCH_ANY, \ - .ec = TPID(__id1, __id2), \ - .quirks = (__quirk) } - -#define TPACPI_QEC_LNV(__id1, __id2, __quirk) \ - { .vendor = PCI_VENDOR_ID_LENOVO, \ - .bios = TPACPI_MATCH_ANY, \ - .ec = TPID(__id1, __id2), \ - .quirks = (__quirk) } - -struct tpacpi_quirk { - unsigned int vendor; - u32 bios; - u32 ec; - unsigned long quirks; -}; - -/** - * tpacpi_check_quirks() - search BIOS/EC version on a list - * @qlist: array of &struct tpacpi_quirk - * @qlist_size: number of elements in @qlist - * - * Iterates over a quirks list until one is found that matches the - * ThinkPad's vendor, BIOS and EC model. - * - * Returns: %0 if nothing matches, otherwise returns the quirks field of - * the matching &struct tpacpi_quirk entry. - * - * The match criteria is: vendor, ec and bios must match. - */ -static unsigned long __init tpacpi_check_quirks( - const struct tpacpi_quirk *qlist, - unsigned int qlist_size) -{ - while (qlist_size) { - if ((qlist->vendor == thinkpad_id.vendor || - qlist->vendor == TPACPI_MATCH_ANY) && - (qlist->bios == thinkpad_id.bios_model || - qlist->bios == TPACPI_MATCH_ANY) && - (qlist->ec == thinkpad_id.ec_model || - qlist->ec == TPACPI_MATCH_ANY)) - return qlist->quirks; - - qlist_size--; - qlist++; - } - return 0; -} - -static inline bool __pure __init tpacpi_is_lenovo(void) -{ - return thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO; -} - -static inline bool __pure __init tpacpi_is_ibm(void) -{ - return thinkpad_id.vendor == PCI_VENDOR_ID_IBM; -} - -/**************************************************************************** - **************************************************************************** - * - * ACPI Helpers and device model - * - **************************************************************************** - ****************************************************************************/ - -/************************************************************************* - * ACPI basic handles - */ - -static acpi_handle root_handle; -static acpi_handle ec_handle; - -#define TPACPI_HANDLE(object, parent, paths...) \ - static acpi_handle object##_handle; \ - static const acpi_handle * const object##_parent __initconst = \ - &parent##_handle; \ - static char *object##_paths[] __initdata = { paths } - -TPACPI_HANDLE(ecrd, ec, "ECRD"); /* 570 */ -TPACPI_HANDLE(ecwr, ec, "ECWR"); /* 570 */ - -TPACPI_HANDLE(cmos, root, "\\UCMS", /* R50, R50e, R50p, R51, */ - /* T4x, X31, X40 */ - "\\CMOS", /* A3x, G4x, R32, T23, T30, X22-24, X30 */ - "\\CMS", /* R40, R40e */ - ); /* all others */ - -TPACPI_HANDLE(hkey, ec, "\\_SB.HKEY", /* 600e/x, 770e, 770x */ - "^HKEY", /* R30, R31 */ - "HKEY", /* all others */ - ); /* 570 */ - -/************************************************************************* - * ACPI helpers - */ - -static int acpi_evalf(acpi_handle handle, - int *res, char *method, char *fmt, ...) -{ - char *fmt0 = fmt; - struct acpi_object_list params; - union acpi_object in_objs[TPACPI_MAX_ACPI_ARGS]; - struct acpi_buffer result, *resultp; - union acpi_object out_obj; - acpi_status status; - va_list ap; - char res_type; - int success; - int quiet; - - if (!*fmt) { - pr_err("acpi_evalf() called with empty format\n"); - return 0; - } - - if (*fmt == 'q') { - quiet = 1; - fmt++; - } else - quiet = 0; - - res_type = *(fmt++); - - params.count = 0; - params.pointer = &in_objs[0]; - - va_start(ap, fmt); - while (*fmt) { - char c = *(fmt++); - switch (c) { - case 'd': /* int */ - in_objs[params.count].integer.value = va_arg(ap, int); - in_objs[params.count++].type = ACPI_TYPE_INTEGER; - break; - /* add more types as needed */ - default: - pr_err("acpi_evalf() called with invalid format character '%c'\n", - c); - va_end(ap); - return 0; - } - } - va_end(ap); - - if (res_type != 'v') { - result.length = sizeof(out_obj); - result.pointer = &out_obj; - resultp = &result; - } else - resultp = NULL; - - status = acpi_evaluate_object(handle, method, ¶ms, resultp); - - switch (res_type) { - case 'd': /* int */ - success = (status == AE_OK && - out_obj.type == ACPI_TYPE_INTEGER); - if (success && res) - *res = out_obj.integer.value; - break; - case 'v': /* void */ - success = status == AE_OK; - break; - /* add more types as needed */ - default: - pr_err("acpi_evalf() called with invalid format character '%c'\n", - res_type); - return 0; - } - - if (!success && !quiet) - pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", - method, fmt0, acpi_format_exception(status)); - - return success; -} - -static int acpi_ec_read(int i, u8 *p) -{ - int v; - - if (ecrd_handle) { - if (!acpi_evalf(ecrd_handle, &v, NULL, "dd", i)) - return 0; - *p = v; - } else { - if (ec_read(i, p) < 0) - return 0; - } - - return 1; -} - -static int acpi_ec_write(int i, u8 v) -{ - if (ecwr_handle) { - if (!acpi_evalf(ecwr_handle, NULL, NULL, "vdd", i, v)) - return 0; - } else { - if (ec_write(i, v) < 0) - return 0; - } - - return 1; -} - -static int issue_thinkpad_cmos_command(int cmos_cmd) -{ - if (!cmos_handle) - return -ENXIO; - - if (!acpi_evalf(cmos_handle, NULL, NULL, "vd", cmos_cmd)) - return -EIO; - - return 0; -} - -/************************************************************************* - * ACPI device model - */ - -#define TPACPI_ACPIHANDLE_INIT(object) \ - drv_acpi_handle_init(#object, &object##_handle, *object##_parent, \ - object##_paths, ARRAY_SIZE(object##_paths)) - -static void __init drv_acpi_handle_init(const char *name, - acpi_handle *handle, const acpi_handle parent, - char **paths, const int num_paths) -{ - int i; - acpi_status status; - - vdbg_printk(TPACPI_DBG_INIT, "trying to locate ACPI handle for %s\n", - name); - - for (i = 0; i < num_paths; i++) { - status = acpi_get_handle(parent, paths[i], handle); - if (ACPI_SUCCESS(status)) { - dbg_printk(TPACPI_DBG_INIT, - "Found ACPI handle %s for %s\n", - paths[i], name); - return; - } - } - - vdbg_printk(TPACPI_DBG_INIT, "ACPI handle for %s not found\n", - name); - *handle = NULL; -} - -static acpi_status __init tpacpi_acpi_handle_locate_callback(acpi_handle handle, - u32 level, void *context, void **return_value) -{ - if (!strcmp(context, "video")) { - struct acpi_device *dev = acpi_fetch_acpi_dev(handle); - - if (!dev || strcmp(ACPI_VIDEO_HID, acpi_device_hid(dev))) - return AE_OK; - } - - *(acpi_handle *)return_value = handle; - - return AE_CTRL_TERMINATE; -} - -static void __init tpacpi_acpi_handle_locate(const char *name, - const char *hid, - acpi_handle *handle) -{ - acpi_status status; - acpi_handle device_found; - - BUG_ON(!name || !handle); - vdbg_printk(TPACPI_DBG_INIT, - "trying to locate ACPI handle for %s, using HID %s\n", - name, hid ? hid : "NULL"); - - memset(&device_found, 0, sizeof(device_found)); - status = acpi_get_devices(hid, tpacpi_acpi_handle_locate_callback, - (void *)name, &device_found); - - *handle = NULL; - - if (ACPI_SUCCESS(status)) { - *handle = device_found; - dbg_printk(TPACPI_DBG_INIT, - "Found ACPI handle for %s\n", name); - } else { - vdbg_printk(TPACPI_DBG_INIT, - "Could not locate an ACPI handle for %s: %s\n", - name, acpi_format_exception(status)); - } -} - -static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) -{ - struct ibm_struct *ibm = data; - - if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) - return; - - if (!ibm || !ibm->acpi || !ibm->acpi->notify) - return; - - ibm->acpi->notify(ibm, event); -} - -static int __init setup_acpi_notify(struct ibm_struct *ibm) -{ - acpi_status status; - - BUG_ON(!ibm->acpi); - - if (!*ibm->acpi->handle) - return 0; - - vdbg_printk(TPACPI_DBG_INIT, - "setting up ACPI notify for %s\n", ibm->name); - - ibm->acpi->device = acpi_fetch_acpi_dev(*ibm->acpi->handle); - if (!ibm->acpi->device) { - pr_err("acpi_fetch_acpi_dev(%s) failed\n", ibm->name); - return -ENODEV; - } - - ibm->acpi->device->driver_data = ibm; - scnprintf(acpi_device_class(ibm->acpi->device), - sizeof(acpi_device_class(ibm->acpi->device)), - "%s/%s", TPACPI_ACPI_EVENT_PREFIX, ibm->name); - - status = acpi_install_notify_handler(*ibm->acpi->handle, - ibm->acpi->type, dispatch_acpi_notify, ibm); - if (ACPI_FAILURE(status)) { - if (status == AE_ALREADY_EXISTS) { - pr_notice("another device driver is already handling %s events\n", - ibm->name); - } else { - pr_err("acpi_install_notify_handler(%s) failed: %s\n", - ibm->name, acpi_format_exception(status)); - } - return -ENODEV; - } - ibm->flags.acpi_notify_installed = 1; - return 0; -} - -static int __init tpacpi_device_add(struct acpi_device *device) -{ - return 0; -} - -static int __init register_tpacpi_subdriver(struct ibm_struct *ibm) -{ - int rc; - - dbg_printk(TPACPI_DBG_INIT, - "registering %s as an ACPI driver\n", ibm->name); - - BUG_ON(!ibm->acpi); - - ibm->acpi->driver = kzalloc(sizeof(struct acpi_driver), GFP_KERNEL); - if (!ibm->acpi->driver) { - pr_err("failed to allocate memory for ibm->acpi->driver\n"); - return -ENOMEM; - } - - sprintf(ibm->acpi->driver->name, "%s_%s", TPACPI_NAME, ibm->name); - ibm->acpi->driver->ids = ibm->acpi->hid; - - ibm->acpi->driver->ops.add = &tpacpi_device_add; - - rc = acpi_bus_register_driver(ibm->acpi->driver); - if (rc < 0) { - pr_err("acpi_bus_register_driver(%s) failed: %d\n", - ibm->name, rc); - kfree(ibm->acpi->driver); - ibm->acpi->driver = NULL; - } else if (!rc) - ibm->flags.acpi_driver_registered = 1; - - return rc; -} - - -/**************************************************************************** - **************************************************************************** - * - * Procfs Helpers - * - **************************************************************************** - ****************************************************************************/ - -static int dispatch_proc_show(struct seq_file *m, void *v) -{ - struct ibm_struct *ibm = m->private; - - if (!ibm || !ibm->read) - return -EINVAL; - return ibm->read(m); -} - -static int dispatch_proc_open(struct inode *inode, struct file *file) -{ - return single_open(file, dispatch_proc_show, pde_data(inode)); -} - -static ssize_t dispatch_proc_write(struct file *file, - const char __user *userbuf, - size_t count, loff_t *pos) -{ - struct ibm_struct *ibm = pde_data(file_inode(file)); - char *kernbuf; - int ret; - - if (!ibm || !ibm->write) - return -EINVAL; - if (count > PAGE_SIZE - 1) - return -EINVAL; - - kernbuf = memdup_user_nul(userbuf, count); - if (IS_ERR(kernbuf)) - return PTR_ERR(kernbuf); - ret = ibm->write(kernbuf); - if (ret == 0) - ret = count; - - kfree(kernbuf); - - return ret; -} - -static const struct proc_ops dispatch_proc_ops = { - .proc_open = dispatch_proc_open, - .proc_read = seq_read, - .proc_lseek = seq_lseek, - .proc_release = single_release, - .proc_write = dispatch_proc_write, -}; - -/**************************************************************************** - **************************************************************************** - * - * Device model: input, hwmon and platform - * - **************************************************************************** - ****************************************************************************/ - -static struct platform_device *tpacpi_pdev; -static struct platform_device *tpacpi_sensors_pdev; -static struct device *tpacpi_hwmon; -static struct device *tpacpi_pprof; -static struct input_dev *tpacpi_inputdev; -static struct mutex tpacpi_inputdev_send_mutex; -static LIST_HEAD(tpacpi_all_drivers); - -#ifdef CONFIG_PM_SLEEP -static int tpacpi_suspend_handler(struct device *dev) -{ - struct ibm_struct *ibm, *itmp; - - list_for_each_entry_safe(ibm, itmp, - &tpacpi_all_drivers, - all_drivers) { - if (ibm->suspend) - (ibm->suspend)(); - } - - return 0; -} - -static int tpacpi_resume_handler(struct device *dev) -{ - struct ibm_struct *ibm, *itmp; - - list_for_each_entry_safe(ibm, itmp, - &tpacpi_all_drivers, - all_drivers) { - if (ibm->resume) - (ibm->resume)(); - } - - return 0; -} -#endif - -static SIMPLE_DEV_PM_OPS(tpacpi_pm, - tpacpi_suspend_handler, tpacpi_resume_handler); - -static void tpacpi_shutdown_handler(struct platform_device *pdev) -{ - struct ibm_struct *ibm, *itmp; - - list_for_each_entry_safe(ibm, itmp, - &tpacpi_all_drivers, - all_drivers) { - if (ibm->shutdown) - (ibm->shutdown)(); - } -} - -/************************************************************************* - * sysfs support helpers - */ - -static int parse_strtoul(const char *buf, - unsigned long max, unsigned long *value) -{ - char *endp; - - *value = simple_strtoul(skip_spaces(buf), &endp, 0); - endp = skip_spaces(endp); - if (*endp || *value > max) - return -EINVAL; - - return 0; -} - -static void tpacpi_disable_brightness_delay(void) -{ - if (acpi_evalf(hkey_handle, NULL, "PWMS", "qvd", 0)) - pr_notice("ACPI backlight control delay disabled\n"); -} - -static void printk_deprecated_attribute(const char * const what, - const char * const details) -{ - tpacpi_log_usertask("deprecated sysfs attribute"); - pr_warn("WARNING: sysfs attribute %s is deprecated and will be removed. %s\n", - what, details); -} - -/************************************************************************* - * rfkill and radio control support helpers - */ - -/* - * ThinkPad-ACPI firmware handling model: - * - * WLSW (master wireless switch) is event-driven, and is common to all - * firmware-controlled radios. It cannot be controlled, just monitored, - * as expected. It overrides all radio state in firmware - * - * The kernel, a masked-off hotkey, and WLSW can change the radio state - * (TODO: verify how WLSW interacts with the returned radio state). - * - * The only time there are shadow radio state changes, is when - * masked-off hotkeys are used. - */ - -/* - * Internal driver API for radio state: - * - * int: < 0 = error, otherwise enum tpacpi_rfkill_state - * bool: true means radio blocked (off) - */ -enum tpacpi_rfkill_state { - TPACPI_RFK_RADIO_OFF = 0, - TPACPI_RFK_RADIO_ON -}; - -/* rfkill switches */ -enum tpacpi_rfk_id { - TPACPI_RFK_BLUETOOTH_SW_ID = 0, - TPACPI_RFK_WWAN_SW_ID, - TPACPI_RFK_UWB_SW_ID, - TPACPI_RFK_SW_MAX -}; - -static const char *tpacpi_rfkill_names[] = { - [TPACPI_RFK_BLUETOOTH_SW_ID] = "bluetooth", - [TPACPI_RFK_WWAN_SW_ID] = "wwan", - [TPACPI_RFK_UWB_SW_ID] = "uwb", - [TPACPI_RFK_SW_MAX] = NULL -}; - -/* ThinkPad-ACPI rfkill subdriver */ -struct tpacpi_rfk { - struct rfkill *rfkill; - enum tpacpi_rfk_id id; - const struct tpacpi_rfk_ops *ops; -}; - -struct tpacpi_rfk_ops { - /* firmware interface */ - int (*get_status)(void); - int (*set_status)(const enum tpacpi_rfkill_state); -}; - -static struct tpacpi_rfk *tpacpi_rfkill_switches[TPACPI_RFK_SW_MAX]; - -/* Query FW and update rfkill sw state for a given rfkill switch */ -static int tpacpi_rfk_update_swstate(const struct tpacpi_rfk *tp_rfk) -{ - int status; - - if (!tp_rfk) - return -ENODEV; - - status = (tp_rfk->ops->get_status)(); - if (status < 0) - return status; - - rfkill_set_sw_state(tp_rfk->rfkill, - (status == TPACPI_RFK_RADIO_OFF)); - - return status; -} - -/* - * Sync the HW-blocking state of all rfkill switches, - * do notice it causes the rfkill core to schedule uevents - */ -static void tpacpi_rfk_update_hwblock_state(bool blocked) -{ - unsigned int i; - struct tpacpi_rfk *tp_rfk; - - for (i = 0; i < TPACPI_RFK_SW_MAX; i++) { - tp_rfk = tpacpi_rfkill_switches[i]; - if (tp_rfk) { - if (rfkill_set_hw_state(tp_rfk->rfkill, - blocked)) { - /* ignore -- we track sw block */ - } - } - } -} - -/* Call to get the WLSW state from the firmware */ -static int hotkey_get_wlsw(void); - -/* Call to query WLSW state and update all rfkill switches */ -static bool tpacpi_rfk_check_hwblock_state(void) -{ - int res = hotkey_get_wlsw(); - int hw_blocked; - - /* When unknown or unsupported, we have to assume it is unblocked */ - if (res < 0) - return false; - - hw_blocked = (res == TPACPI_RFK_RADIO_OFF); - tpacpi_rfk_update_hwblock_state(hw_blocked); - - return hw_blocked; -} - -static int tpacpi_rfk_hook_set_block(void *data, bool blocked) -{ - struct tpacpi_rfk *tp_rfk = data; - int res; - - dbg_printk(TPACPI_DBG_RFKILL, - "request to change radio state to %s\n", - blocked ? "blocked" : "unblocked"); - - /* try to set radio state */ - res = (tp_rfk->ops->set_status)(blocked ? - TPACPI_RFK_RADIO_OFF : TPACPI_RFK_RADIO_ON); - - /* and update the rfkill core with whatever the FW really did */ - tpacpi_rfk_update_swstate(tp_rfk); - - return (res < 0) ? res : 0; -} - -static const struct rfkill_ops tpacpi_rfk_rfkill_ops = { - .set_block = tpacpi_rfk_hook_set_block, -}; - -static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id, - const struct tpacpi_rfk_ops *tp_rfkops, - const enum rfkill_type rfktype, - const char *name, - const bool set_default) -{ - struct tpacpi_rfk *atp_rfk; - int res; - bool sw_state = false; - bool hw_state; - int sw_status; - - BUG_ON(id >= TPACPI_RFK_SW_MAX || tpacpi_rfkill_switches[id]); - - atp_rfk = kzalloc(sizeof(struct tpacpi_rfk), GFP_KERNEL); - if (atp_rfk) - atp_rfk->rfkill = rfkill_alloc(name, - &tpacpi_pdev->dev, - rfktype, - &tpacpi_rfk_rfkill_ops, - atp_rfk); - if (!atp_rfk || !atp_rfk->rfkill) { - pr_err("failed to allocate memory for rfkill class\n"); - kfree(atp_rfk); - return -ENOMEM; - } - - atp_rfk->id = id; - atp_rfk->ops = tp_rfkops; - - sw_status = (tp_rfkops->get_status)(); - if (sw_status < 0) { - pr_err("failed to read initial state for %s, error %d\n", - name, sw_status); - } else { - sw_state = (sw_status == TPACPI_RFK_RADIO_OFF); - if (set_default) { - /* try to keep the initial state, since we ask the - * firmware to preserve it across S5 in NVRAM */ - rfkill_init_sw_state(atp_rfk->rfkill, sw_state); - } - } - hw_state = tpacpi_rfk_check_hwblock_state(); - rfkill_set_hw_state(atp_rfk->rfkill, hw_state); - - res = rfkill_register(atp_rfk->rfkill); - if (res < 0) { - pr_err("failed to register %s rfkill switch: %d\n", name, res); - rfkill_destroy(atp_rfk->rfkill); - kfree(atp_rfk); - return res; - } - - tpacpi_rfkill_switches[id] = atp_rfk; - - pr_info("rfkill switch %s: radio is %sblocked\n", - name, (sw_state || hw_state) ? "" : "un"); - return 0; -} - -static void tpacpi_destroy_rfkill(const enum tpacpi_rfk_id id) -{ - struct tpacpi_rfk *tp_rfk; - - BUG_ON(id >= TPACPI_RFK_SW_MAX); - - tp_rfk = tpacpi_rfkill_switches[id]; - if (tp_rfk) { - rfkill_unregister(tp_rfk->rfkill); - rfkill_destroy(tp_rfk->rfkill); - tpacpi_rfkill_switches[id] = NULL; - kfree(tp_rfk); - } -} - -static void printk_deprecated_rfkill_attribute(const char * const what) -{ - printk_deprecated_attribute(what, - "Please switch to generic rfkill before year 2010"); -} - -/* sysfs enable ------------------------------------------------ */ -static ssize_t tpacpi_rfk_sysfs_enable_show(const enum tpacpi_rfk_id id, - struct device_attribute *attr, - char *buf) -{ - int status; - - printk_deprecated_rfkill_attribute(attr->attr.name); - - /* This is in the ABI... */ - if (tpacpi_rfk_check_hwblock_state()) { - status = TPACPI_RFK_RADIO_OFF; - } else { - status = tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); - if (status < 0) - return status; - } - - return sysfs_emit(buf, "%d\n", - (status == TPACPI_RFK_RADIO_ON) ? 1 : 0); -} - -static ssize_t tpacpi_rfk_sysfs_enable_store(const enum tpacpi_rfk_id id, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - int res; - - printk_deprecated_rfkill_attribute(attr->attr.name); - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - tpacpi_disclose_usertask(attr->attr.name, "set to %ld\n", t); - - /* This is in the ABI... */ - if (tpacpi_rfk_check_hwblock_state() && !!t) - return -EPERM; - - res = tpacpi_rfkill_switches[id]->ops->set_status((!!t) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF); - tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); - - return (res < 0) ? res : count; -} - -/* procfs -------------------------------------------------------------- */ -static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, struct seq_file *m) -{ - if (id >= TPACPI_RFK_SW_MAX) - seq_printf(m, "status:\t\tnot supported\n"); - else { - int status; - - /* This is in the ABI... */ - if (tpacpi_rfk_check_hwblock_state()) { - status = TPACPI_RFK_RADIO_OFF; - } else { - status = tpacpi_rfk_update_swstate( - tpacpi_rfkill_switches[id]); - if (status < 0) - return status; - } - - seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status == TPACPI_RFK_RADIO_ON)); - seq_printf(m, "commands:\tenable, disable\n"); - } - - return 0; -} - -static int tpacpi_rfk_procfs_write(const enum tpacpi_rfk_id id, char *buf) -{ - char *cmd; - int status = -1; - int res = 0; - - if (id >= TPACPI_RFK_SW_MAX) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - if (strstarts(cmd, "enable")) - status = TPACPI_RFK_RADIO_ON; - else if (strstarts(cmd, "disable")) - status = TPACPI_RFK_RADIO_OFF; - else - return -EINVAL; - } - - if (status != -1) { - tpacpi_disclose_usertask("procfs", "attempt to %s %s\n", - str_enable_disable(status == TPACPI_RFK_RADIO_ON), - tpacpi_rfkill_names[id]); - res = (tpacpi_rfkill_switches[id]->ops->set_status)(status); - tpacpi_rfk_update_swstate(tpacpi_rfkill_switches[id]); - } - - return res; -} - -/************************************************************************* - * thinkpad-acpi driver attributes - */ - -/* interface_version --------------------------------------------------- */ -static ssize_t interface_version_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", TPACPI_SYSFS_VERSION); -} -static DRIVER_ATTR_RO(interface_version); - -/* debug_level --------------------------------------------------------- */ -static ssize_t debug_level_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "0x%04x\n", dbg_level); -} - -static ssize_t debug_level_store(struct device_driver *drv, const char *buf, - size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 0xffff, &t)) - return -EINVAL; - - dbg_level = t; - - return count; -} -static DRIVER_ATTR_RW(debug_level); - -/* version ------------------------------------------------------------- */ -static ssize_t version_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%s v%s\n", - TPACPI_DESC, TPACPI_VERSION); -} -static DRIVER_ATTR_RO(version); - -/* --------------------------------------------------------------------- */ - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - -/* wlsw_emulstate ------------------------------------------------------ */ -static ssize_t wlsw_emulstate_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%d\n", !!tpacpi_wlsw_emulstate); -} - -static ssize_t wlsw_emulstate_store(struct device_driver *drv, const char *buf, - size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - if (tpacpi_wlsw_emulstate != !!t) { - tpacpi_wlsw_emulstate = !!t; - tpacpi_rfk_update_hwblock_state(!t); /* negative logic */ - } - - return count; -} -static DRIVER_ATTR_RW(wlsw_emulstate); - -/* bluetooth_emulstate ------------------------------------------------- */ -static ssize_t bluetooth_emulstate_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%d\n", !!tpacpi_bluetooth_emulstate); -} - -static ssize_t bluetooth_emulstate_store(struct device_driver *drv, - const char *buf, size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - tpacpi_bluetooth_emulstate = !!t; - - return count; -} -static DRIVER_ATTR_RW(bluetooth_emulstate); - -/* wwan_emulstate ------------------------------------------------- */ -static ssize_t wwan_emulstate_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%d\n", !!tpacpi_wwan_emulstate); -} - -static ssize_t wwan_emulstate_store(struct device_driver *drv, const char *buf, - size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - tpacpi_wwan_emulstate = !!t; - - return count; -} -static DRIVER_ATTR_RW(wwan_emulstate); - -/* uwb_emulstate ------------------------------------------------- */ -static ssize_t uwb_emulstate_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%d\n", !!tpacpi_uwb_emulstate); -} - -static ssize_t uwb_emulstate_store(struct device_driver *drv, const char *buf, - size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - tpacpi_uwb_emulstate = !!t; - - return count; -} -static DRIVER_ATTR_RW(uwb_emulstate); -#endif - -/************************************************************************* - * Firmware Data - */ - -/* - * Table of recommended minimum BIOS versions - * - * Reasons for listing: - * 1. Stable BIOS, listed because the unknown amount of - * bugs and bad ACPI behaviour on older versions - * - * 2. BIOS or EC fw with known bugs that trigger on Linux - * - * 3. BIOS with known reduced functionality in older versions - * - * We recommend the latest BIOS and EC version. - * We only support the latest BIOS and EC fw version as a rule. - * - * Sources: IBM ThinkPad Public Web Documents (update changelogs), - * Information from users in ThinkWiki - * - * WARNING: we use this table also to detect that the machine is - * a ThinkPad in some cases, so don't remove entries lightly. - */ - -#define TPV_Q(__v, __id1, __id2, __bv1, __bv2) \ - { .vendor = (__v), \ - .bios = TPID(__id1, __id2), \ - .ec = TPACPI_MATCH_ANY, \ - .quirks = TPACPI_MATCH_ANY_VERSION << 16 \ - | TPVER(__bv1, __bv2) } - -#define TPV_Q_X(__v, __bid1, __bid2, __bv1, __bv2, \ - __eid, __ev1, __ev2) \ - { .vendor = (__v), \ - .bios = TPID(__bid1, __bid2), \ - .ec = __eid, \ - .quirks = TPVER(__ev1, __ev2) << 16 \ - | TPVER(__bv1, __bv2) } - -#define TPV_QI0(__id1, __id2, __bv1, __bv2) \ - TPV_Q(PCI_VENDOR_ID_IBM, __id1, __id2, __bv1, __bv2) - -/* Outdated IBM BIOSes often lack the EC id string */ -#define TPV_QI1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ - TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ - __bv1, __bv2, TPID(__id1, __id2), \ - __ev1, __ev2), \ - TPV_Q_X(PCI_VENDOR_ID_IBM, __id1, __id2, \ - __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ - __ev1, __ev2) - -/* Outdated IBM BIOSes often lack the EC id string */ -#define TPV_QI2(__bid1, __bid2, __bv1, __bv2, \ - __eid1, __eid2, __ev1, __ev2) \ - TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ - __bv1, __bv2, TPID(__eid1, __eid2), \ - __ev1, __ev2), \ - TPV_Q_X(PCI_VENDOR_ID_IBM, __bid1, __bid2, \ - __bv1, __bv2, TPACPI_MATCH_UNKNOWN, \ - __ev1, __ev2) - -#define TPV_QL0(__id1, __id2, __bv1, __bv2) \ - TPV_Q(PCI_VENDOR_ID_LENOVO, __id1, __id2, __bv1, __bv2) - -#define TPV_QL1(__id1, __id2, __bv1, __bv2, __ev1, __ev2) \ - TPV_Q_X(PCI_VENDOR_ID_LENOVO, __id1, __id2, \ - __bv1, __bv2, TPID(__id1, __id2), \ - __ev1, __ev2) - -#define TPV_QL2(__bid1, __bid2, __bv1, __bv2, \ - __eid1, __eid2, __ev1, __ev2) \ - TPV_Q_X(PCI_VENDOR_ID_LENOVO, __bid1, __bid2, \ - __bv1, __bv2, TPID(__eid1, __eid2), \ - __ev1, __ev2) - -static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = { - /* Numeric models ------------------ */ - /* FW MODEL BIOS VERS */ - TPV_QI0('I', 'M', '6', '5'), /* 570 */ - TPV_QI0('I', 'U', '2', '6'), /* 570E */ - TPV_QI0('I', 'B', '5', '4'), /* 600 */ - TPV_QI0('I', 'H', '4', '7'), /* 600E */ - TPV_QI0('I', 'N', '3', '6'), /* 600E */ - TPV_QI0('I', 'T', '5', '5'), /* 600X */ - TPV_QI0('I', 'D', '4', '8'), /* 770, 770E, 770ED */ - TPV_QI0('I', 'I', '4', '2'), /* 770X */ - TPV_QI0('I', 'O', '2', '3'), /* 770Z */ - - /* A-series ------------------------- */ - /* FW MODEL BIOS VERS EC VERS */ - TPV_QI0('I', 'W', '5', '9'), /* A20m */ - TPV_QI0('I', 'V', '6', '9'), /* A20p */ - TPV_QI0('1', '0', '2', '6'), /* A21e, A22e */ - TPV_QI0('K', 'U', '3', '6'), /* A21e */ - TPV_QI0('K', 'X', '3', '6'), /* A21m, A22m */ - TPV_QI0('K', 'Y', '3', '8'), /* A21p, A22p */ - TPV_QI0('1', 'B', '1', '7'), /* A22e */ - TPV_QI0('1', '3', '2', '0'), /* A22m */ - TPV_QI0('1', 'E', '7', '3'), /* A30/p (0) */ - TPV_QI1('1', 'G', '4', '1', '1', '7'), /* A31/p (0) */ - TPV_QI1('1', 'N', '1', '6', '0', '7'), /* A31/p (0) */ - - /* G-series ------------------------- */ - /* FW MODEL BIOS VERS */ - TPV_QI0('1', 'T', 'A', '6'), /* G40 */ - TPV_QI0('1', 'X', '5', '7'), /* G41 */ - - /* R-series, T-series --------------- */ - /* FW MODEL BIOS VERS EC VERS */ - TPV_QI0('1', 'C', 'F', '0'), /* R30 */ - TPV_QI0('1', 'F', 'F', '1'), /* R31 */ - TPV_QI0('1', 'M', '9', '7'), /* R32 */ - TPV_QI0('1', 'O', '6', '1'), /* R40 */ - TPV_QI0('1', 'P', '6', '5'), /* R40 */ - TPV_QI0('1', 'S', '7', '0'), /* R40e */ - TPV_QI1('1', 'R', 'D', 'R', '7', '1'), /* R50/p, R51, - T40/p, T41/p, T42/p (1) */ - TPV_QI1('1', 'V', '7', '1', '2', '8'), /* R50e, R51 (1) */ - TPV_QI1('7', '8', '7', '1', '0', '6'), /* R51e (1) */ - TPV_QI1('7', '6', '6', '9', '1', '6'), /* R52 (1) */ - TPV_QI1('7', '0', '6', '9', '2', '8'), /* R52, T43 (1) */ - - TPV_QI0('I', 'Y', '6', '1'), /* T20 */ - TPV_QI0('K', 'Z', '3', '4'), /* T21 */ - TPV_QI0('1', '6', '3', '2'), /* T22 */ - TPV_QI1('1', 'A', '6', '4', '2', '3'), /* T23 (0) */ - TPV_QI1('1', 'I', '7', '1', '2', '0'), /* T30 (0) */ - TPV_QI1('1', 'Y', '6', '5', '2', '9'), /* T43/p (1) */ - - TPV_QL1('7', '9', 'E', '3', '5', '0'), /* T60/p */ - TPV_QL1('7', 'C', 'D', '2', '2', '2'), /* R60, R60i */ - TPV_QL1('7', 'E', 'D', '0', '1', '5'), /* R60e, R60i */ - - /* BIOS FW BIOS VERS EC FW EC VERS */ - TPV_QI2('1', 'W', '9', '0', '1', 'V', '2', '8'), /* R50e (1) */ - TPV_QL2('7', 'I', '3', '4', '7', '9', '5', '0'), /* T60/p wide */ - - /* X-series ------------------------- */ - /* FW MODEL BIOS VERS EC VERS */ - TPV_QI0('I', 'Z', '9', 'D'), /* X20, X21 */ - TPV_QI0('1', 'D', '7', '0'), /* X22, X23, X24 */ - TPV_QI1('1', 'K', '4', '8', '1', '8'), /* X30 (0) */ - TPV_QI1('1', 'Q', '9', '7', '2', '3'), /* X31, X32 (0) */ - TPV_QI1('1', 'U', 'D', '3', 'B', '2'), /* X40 (0) */ - TPV_QI1('7', '4', '6', '4', '2', '7'), /* X41 (0) */ - TPV_QI1('7', '5', '6', '0', '2', '0'), /* X41t (0) */ - - TPV_QL1('7', 'B', 'D', '7', '4', '0'), /* X60/s */ - TPV_QL1('7', 'J', '3', '0', '1', '3'), /* X60t */ - - /* (0) - older versions lack DMI EC fw string and functionality */ - /* (1) - older versions known to lack functionality */ -}; - -#undef TPV_QL1 -#undef TPV_QL0 -#undef TPV_QI2 -#undef TPV_QI1 -#undef TPV_QI0 -#undef TPV_Q_X -#undef TPV_Q - -static void __init tpacpi_check_outdated_fw(void) -{ - unsigned long fwvers; - u16 ec_version, bios_version; - - fwvers = tpacpi_check_quirks(tpacpi_bios_version_qtable, - ARRAY_SIZE(tpacpi_bios_version_qtable)); - - if (!fwvers) - return; - - bios_version = fwvers & 0xffffU; - ec_version = (fwvers >> 16) & 0xffffU; - - /* note that unknown versions are set to 0x0000 and we use that */ - if ((bios_version > thinkpad_id.bios_release) || - (ec_version > thinkpad_id.ec_release && - ec_version != TPACPI_MATCH_ANY_VERSION)) { - /* - * The changelogs would let us track down the exact - * reason, but it is just too much of a pain to track - * it. We only list BIOSes that are either really - * broken, or really stable to begin with, so it is - * best if the user upgrades the firmware anyway. - */ - pr_warn("WARNING: Outdated ThinkPad BIOS/EC firmware\n"); - pr_warn("WARNING: This firmware may be missing critical bug fixes and/or important features\n"); - } -} - -static bool __init tpacpi_is_fw_known(void) -{ - return tpacpi_check_quirks(tpacpi_bios_version_qtable, - ARRAY_SIZE(tpacpi_bios_version_qtable)) != 0; -} - -/**************************************************************************** - **************************************************************************** - * - * Subdrivers - * - **************************************************************************** - ****************************************************************************/ - -/************************************************************************* - * thinkpad-acpi metadata subdriver - */ - -static int thinkpad_acpi_driver_read(struct seq_file *m) -{ - seq_printf(m, "driver:\t\t%s\n", TPACPI_DESC); - seq_printf(m, "version:\t%s\n", TPACPI_VERSION); - return 0; -} - -static struct ibm_struct thinkpad_acpi_driver_data = { - .name = "driver", - .read = thinkpad_acpi_driver_read, -}; - -/************************************************************************* - * Hotkey subdriver - */ - -/* - * ThinkPad firmware event model - * - * The ThinkPad firmware has two main event interfaces: normal ACPI - * notifications (which follow the ACPI standard), and a private event - * interface. - * - * The private event interface also issues events for the hotkeys. As - * the driver gained features, the event handling code ended up being - * built around the hotkey subdriver. This will need to be refactored - * to a more formal event API eventually. - * - * Some "hotkeys" are actually supposed to be used as event reports, - * such as "brightness has changed", "volume has changed", depending on - * the ThinkPad model and how the firmware is operating. - * - * Unlike other classes, hotkey-class events have mask/unmask control on - * non-ancient firmware. However, how it behaves changes a lot with the - * firmware model and version. - */ - -enum { /* hot key scan codes (derived from ACPI DSDT) */ - TP_ACPI_HOTKEYSCAN_FNF1 = 0, - TP_ACPI_HOTKEYSCAN_FNF2, - TP_ACPI_HOTKEYSCAN_FNF3, - TP_ACPI_HOTKEYSCAN_FNF4, - TP_ACPI_HOTKEYSCAN_FNF5, - TP_ACPI_HOTKEYSCAN_FNF6, - TP_ACPI_HOTKEYSCAN_FNF7, - TP_ACPI_HOTKEYSCAN_FNF8, - TP_ACPI_HOTKEYSCAN_FNF9, - TP_ACPI_HOTKEYSCAN_FNF10, - TP_ACPI_HOTKEYSCAN_FNF11, - TP_ACPI_HOTKEYSCAN_FNF12, - TP_ACPI_HOTKEYSCAN_FNBACKSPACE, - TP_ACPI_HOTKEYSCAN_FNINSERT, - TP_ACPI_HOTKEYSCAN_FNDELETE, - TP_ACPI_HOTKEYSCAN_FNHOME, - TP_ACPI_HOTKEYSCAN_FNEND, - TP_ACPI_HOTKEYSCAN_FNPAGEUP, - TP_ACPI_HOTKEYSCAN_FNPAGEDOWN, - TP_ACPI_HOTKEYSCAN_FNSPACE, - TP_ACPI_HOTKEYSCAN_VOLUMEUP, - TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, - TP_ACPI_HOTKEYSCAN_MUTE, - TP_ACPI_HOTKEYSCAN_THINKPAD, - TP_ACPI_HOTKEYSCAN_UNK1, - TP_ACPI_HOTKEYSCAN_UNK2, - TP_ACPI_HOTKEYSCAN_MICMUTE, - TP_ACPI_HOTKEYSCAN_UNK4, - TP_ACPI_HOTKEYSCAN_CONFIG, - TP_ACPI_HOTKEYSCAN_SEARCH, - TP_ACPI_HOTKEYSCAN_SCALE, - TP_ACPI_HOTKEYSCAN_FILE, - - /* Adaptive keyboard keycodes */ - TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, /* 32 / 0x20 */ - TP_ACPI_HOTKEYSCAN_MUTE2 = TP_ACPI_HOTKEYSCAN_ADAPTIVE_START, - TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, - TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, - TP_ACPI_HOTKEYSCAN_CLOUD, - TP_ACPI_HOTKEYSCAN_UNK9, - TP_ACPI_HOTKEYSCAN_VOICE, - TP_ACPI_HOTKEYSCAN_UNK10, - TP_ACPI_HOTKEYSCAN_GESTURES, - TP_ACPI_HOTKEYSCAN_UNK11, - TP_ACPI_HOTKEYSCAN_UNK12, - TP_ACPI_HOTKEYSCAN_UNK13, - TP_ACPI_HOTKEYSCAN_CONFIG2, - TP_ACPI_HOTKEYSCAN_NEW_TAB, - TP_ACPI_HOTKEYSCAN_RELOAD, - TP_ACPI_HOTKEYSCAN_BACK, - TP_ACPI_HOTKEYSCAN_MIC_DOWN, - TP_ACPI_HOTKEYSCAN_MIC_UP, - TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, - TP_ACPI_HOTKEYSCAN_CAMERA_MODE, - TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, - - /* Lenovo extended keymap, starting at 0x1300 */ - TP_ACPI_HOTKEYSCAN_EXTENDED_START, /* 52 / 0x34 */ - /* first new observed key (star, favorites) is 0x1311 */ - TP_ACPI_HOTKEYSCAN_STAR = 69, - TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, - TP_ACPI_HOTKEYSCAN_CALCULATOR, - TP_ACPI_HOTKEYSCAN_BLUETOOTH, - TP_ACPI_HOTKEYSCAN_KEYBOARD, - TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, /* Used by "Lenovo Quick Clean" */ - TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, - TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, - TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, -}; - -enum { /* Keys/events available through NVRAM polling */ - TPACPI_HKEY_NVRAM_KNOWN_MASK = 0x00fb88c0U, - TPACPI_HKEY_NVRAM_GOOD_MASK = 0x00fb8000U, -}; - -enum { /* Positions of some of the keys in hotkey masks */ - TP_ACPI_HKEY_DISPSWTCH_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF7, - TP_ACPI_HKEY_DISPXPAND_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF8, - TP_ACPI_HKEY_HIBERNATE_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNF12, - TP_ACPI_HKEY_BRGHTUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNHOME, - TP_ACPI_HKEY_BRGHTDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNEND, - TP_ACPI_HKEY_KBD_LIGHT_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNPAGEUP, - TP_ACPI_HKEY_ZOOM_MASK = 1 << TP_ACPI_HOTKEYSCAN_FNSPACE, - TP_ACPI_HKEY_VOLUP_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEUP, - TP_ACPI_HKEY_VOLDWN_MASK = 1 << TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, - TP_ACPI_HKEY_MUTE_MASK = 1 << TP_ACPI_HOTKEYSCAN_MUTE, - TP_ACPI_HKEY_THINKPAD_MASK = 1 << TP_ACPI_HOTKEYSCAN_THINKPAD, -}; - -enum { /* NVRAM to ACPI HKEY group map */ - TP_NVRAM_HKEY_GROUP_HK2 = TP_ACPI_HKEY_THINKPAD_MASK | - TP_ACPI_HKEY_ZOOM_MASK | - TP_ACPI_HKEY_DISPSWTCH_MASK | - TP_ACPI_HKEY_HIBERNATE_MASK, - TP_NVRAM_HKEY_GROUP_BRIGHTNESS = TP_ACPI_HKEY_BRGHTUP_MASK | - TP_ACPI_HKEY_BRGHTDWN_MASK, - TP_NVRAM_HKEY_GROUP_VOLUME = TP_ACPI_HKEY_VOLUP_MASK | - TP_ACPI_HKEY_VOLDWN_MASK | - TP_ACPI_HKEY_MUTE_MASK, -}; - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL -struct tp_nvram_state { - u16 thinkpad_toggle:1; - u16 zoom_toggle:1; - u16 display_toggle:1; - u16 thinklight_toggle:1; - u16 hibernate_toggle:1; - u16 displayexp_toggle:1; - u16 display_state:1; - u16 brightness_toggle:1; - u16 volume_toggle:1; - u16 mute:1; - - u8 brightness_level; - u8 volume_level; -}; - -/* kthread for the hotkey poller */ -static struct task_struct *tpacpi_hotkey_task; - -/* - * Acquire mutex to write poller control variables as an - * atomic block. - * - * Increment hotkey_config_change when changing them if you - * want the kthread to forget old state. - * - * See HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END - */ -static struct mutex hotkey_thread_data_mutex; -static unsigned int hotkey_config_change; - -/* - * hotkey poller control variables - * - * Must be atomic or readers will also need to acquire mutex - * - * HOTKEY_CONFIG_CRITICAL_START/HOTKEY_CONFIG_CRITICAL_END - * should be used only when the changes need to be taken as - * a block, OR when one needs to force the kthread to forget - * old state. - */ -static u32 hotkey_source_mask; /* bit mask 0=ACPI,1=NVRAM */ -static unsigned int hotkey_poll_freq = 10; /* Hz */ - -#define HOTKEY_CONFIG_CRITICAL_START \ - do { \ - mutex_lock(&hotkey_thread_data_mutex); \ - hotkey_config_change++; \ - } while (0); -#define HOTKEY_CONFIG_CRITICAL_END \ - mutex_unlock(&hotkey_thread_data_mutex); - -#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ - -#define hotkey_source_mask 0U -#define HOTKEY_CONFIG_CRITICAL_START -#define HOTKEY_CONFIG_CRITICAL_END - -#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ - -static struct mutex hotkey_mutex; - -static enum { /* Reasons for waking up */ - TP_ACPI_WAKEUP_NONE = 0, /* None or unknown */ - TP_ACPI_WAKEUP_BAYEJ, /* Bay ejection request */ - TP_ACPI_WAKEUP_UNDOCK, /* Undock request */ -} hotkey_wakeup_reason; - -static int hotkey_autosleep_ack; - -static u32 hotkey_orig_mask; /* events the BIOS had enabled */ -static u32 hotkey_all_mask; /* all events supported in fw */ -static u32 hotkey_adaptive_all_mask; /* all adaptive events supported in fw */ -static u32 hotkey_reserved_mask; /* events better left disabled */ -static u32 hotkey_driver_mask; /* events needed by the driver */ -static u32 hotkey_user_mask; /* events visible to userspace */ -static u32 hotkey_acpi_mask; /* events enabled in firmware */ - -static bool tpacpi_driver_event(const unsigned int hkey_event); -static void hotkey_poll_setup(const bool may_warn); - -/* HKEY.MHKG() return bits */ -#define TP_HOTKEY_TABLET_MASK (1 << 3) -enum { - TP_ACPI_MULTI_MODE_INVALID = 0, - TP_ACPI_MULTI_MODE_UNKNOWN = 1 << 0, - TP_ACPI_MULTI_MODE_LAPTOP = 1 << 1, - TP_ACPI_MULTI_MODE_TABLET = 1 << 2, - TP_ACPI_MULTI_MODE_FLAT = 1 << 3, - TP_ACPI_MULTI_MODE_STAND = 1 << 4, - TP_ACPI_MULTI_MODE_TENT = 1 << 5, - TP_ACPI_MULTI_MODE_STAND_TENT = 1 << 6, -}; - -enum { - /* The following modes are considered tablet mode for the purpose of - * reporting the status to userspace. i.e. in all these modes it makes - * sense to disable the laptop input devices such as touchpad and - * keyboard. - */ - TP_ACPI_MULTI_MODE_TABLET_LIKE = TP_ACPI_MULTI_MODE_TABLET | - TP_ACPI_MULTI_MODE_STAND | - TP_ACPI_MULTI_MODE_TENT | - TP_ACPI_MULTI_MODE_STAND_TENT, -}; - -static int hotkey_get_wlsw(void) -{ - int status; - - if (!tp_features.hotkey_wlsw) - return -ENODEV; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_wlswemul) - return (tpacpi_wlsw_emulstate) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -#endif - - if (!acpi_evalf(hkey_handle, &status, "WLSW", "d")) - return -EIO; - - return (status) ? TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -} - -static int hotkey_gmms_get_tablet_mode(int s, int *has_tablet_mode) -{ - int type = (s >> 16) & 0xffff; - int value = s & 0xffff; - int mode = TP_ACPI_MULTI_MODE_INVALID; - int valid_modes = 0; - - if (has_tablet_mode) - *has_tablet_mode = 0; - - switch (type) { - case 1: - valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | - TP_ACPI_MULTI_MODE_TABLET | - TP_ACPI_MULTI_MODE_STAND_TENT; - break; - case 2: - valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | - TP_ACPI_MULTI_MODE_FLAT | - TP_ACPI_MULTI_MODE_TABLET | - TP_ACPI_MULTI_MODE_STAND | - TP_ACPI_MULTI_MODE_TENT; - break; - case 3: - valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | - TP_ACPI_MULTI_MODE_FLAT; - break; - case 4: - case 5: - /* In mode 4, FLAT is not specified as a valid mode. However, - * it can be seen at least on the X1 Yoga 2nd Generation. - */ - valid_modes = TP_ACPI_MULTI_MODE_LAPTOP | - TP_ACPI_MULTI_MODE_FLAT | - TP_ACPI_MULTI_MODE_TABLET | - TP_ACPI_MULTI_MODE_STAND | - TP_ACPI_MULTI_MODE_TENT; - break; - default: - pr_err("Unknown multi mode status type %d with value 0x%04X, please report this to %s\n", - type, value, TPACPI_MAIL); - return 0; - } - - if (has_tablet_mode && (valid_modes & TP_ACPI_MULTI_MODE_TABLET_LIKE)) - *has_tablet_mode = 1; - - switch (value) { - case 1: - mode = TP_ACPI_MULTI_MODE_LAPTOP; - break; - case 2: - mode = TP_ACPI_MULTI_MODE_FLAT; - break; - case 3: - mode = TP_ACPI_MULTI_MODE_TABLET; - break; - case 4: - if (type == 1) - mode = TP_ACPI_MULTI_MODE_STAND_TENT; - else - mode = TP_ACPI_MULTI_MODE_STAND; - break; - case 5: - mode = TP_ACPI_MULTI_MODE_TENT; - break; - default: - if (type == 5 && value == 0xffff) { - pr_warn("Multi mode status is undetected, assuming laptop\n"); - return 0; - } - } - - if (!(mode & valid_modes)) { - pr_err("Unknown/reserved multi mode value 0x%04X for type %d, please report this to %s\n", - value, type, TPACPI_MAIL); - return 0; - } - - return !!(mode & TP_ACPI_MULTI_MODE_TABLET_LIKE); -} - -static int hotkey_get_tablet_mode(int *status) -{ - int s; - - switch (tp_features.hotkey_tablet) { - case TP_HOTKEY_TABLET_USES_MHKG: - if (!acpi_evalf(hkey_handle, &s, "MHKG", "d")) - return -EIO; - - *status = ((s & TP_HOTKEY_TABLET_MASK) != 0); - break; - case TP_HOTKEY_TABLET_USES_GMMS: - if (!acpi_evalf(hkey_handle, &s, "GMMS", "dd", 0)) - return -EIO; - - *status = hotkey_gmms_get_tablet_mode(s, NULL); - break; - default: - break; - } - - return 0; -} - -/* - * Reads current event mask from firmware, and updates - * hotkey_acpi_mask accordingly. Also resets any bits - * from hotkey_user_mask that are unavailable to be - * delivered (shadow requirement of the userspace ABI). - */ -static int hotkey_mask_get(void) -{ - lockdep_assert_held(&hotkey_mutex); - - if (tp_features.hotkey_mask) { - u32 m = 0; - - if (!acpi_evalf(hkey_handle, &m, "DHKN", "d")) - return -EIO; - - hotkey_acpi_mask = m; - } else { - /* no mask support doesn't mean no event support... */ - hotkey_acpi_mask = hotkey_all_mask; - } - - /* sync userspace-visible mask */ - hotkey_user_mask &= (hotkey_acpi_mask | hotkey_source_mask); - - return 0; -} - -static void hotkey_mask_warn_incomplete_mask(void) -{ - /* log only what the user can fix... */ - const u32 wantedmask = hotkey_driver_mask & - ~(hotkey_acpi_mask | hotkey_source_mask) & - (hotkey_all_mask | TPACPI_HKEY_NVRAM_KNOWN_MASK); - - if (wantedmask) - pr_notice("required events 0x%08x not enabled!\n", wantedmask); -} - -/* - * Set the firmware mask when supported - * - * Also calls hotkey_mask_get to update hotkey_acpi_mask. - * - * NOTE: does not set bits in hotkey_user_mask, but may reset them. - */ -static int hotkey_mask_set(u32 mask) -{ - int i; - int rc = 0; - - const u32 fwmask = mask & ~hotkey_source_mask; - - lockdep_assert_held(&hotkey_mutex); - - if (tp_features.hotkey_mask) { - for (i = 0; i < 32; i++) { - if (!acpi_evalf(hkey_handle, - NULL, "MHKM", "vdd", i + 1, - !!(mask & (1 << i)))) { - rc = -EIO; - break; - } - } - } - - /* - * We *must* make an inconditional call to hotkey_mask_get to - * refresh hotkey_acpi_mask and update hotkey_user_mask - * - * Take the opportunity to also log when we cannot _enable_ - * a given event. - */ - if (!hotkey_mask_get() && !rc && (fwmask & ~hotkey_acpi_mask)) { - pr_notice("asked for hotkey mask 0x%08x, but firmware forced it to 0x%08x\n", - fwmask, hotkey_acpi_mask); - } - - if (tpacpi_lifecycle != TPACPI_LIFE_EXITING) - hotkey_mask_warn_incomplete_mask(); - - return rc; -} - -/* - * Sets hotkey_user_mask and tries to set the firmware mask - */ -static int hotkey_user_mask_set(const u32 mask) -{ - int rc; - - lockdep_assert_held(&hotkey_mutex); - - /* Give people a chance to notice they are doing something that - * is bound to go boom on their users sooner or later */ - if (!tp_warned.hotkey_mask_ff && - (mask == 0xffff || mask == 0xffffff || - mask == 0xffffffff)) { - tp_warned.hotkey_mask_ff = 1; - pr_notice("setting the hotkey mask to 0x%08x is likely not the best way to go about it\n", - mask); - pr_notice("please consider using the driver defaults, and refer to up-to-date thinkpad-acpi documentation\n"); - } - - /* Try to enable what the user asked for, plus whatever we need. - * this syncs everything but won't enable bits in hotkey_user_mask */ - rc = hotkey_mask_set((mask | hotkey_driver_mask) & ~hotkey_source_mask); - - /* Enable the available bits in hotkey_user_mask */ - hotkey_user_mask = mask & (hotkey_acpi_mask | hotkey_source_mask); - - return rc; -} - -/* - * Sets the driver hotkey mask. - * - * Can be called even if the hotkey subdriver is inactive - */ -static int tpacpi_hotkey_driver_mask_set(const u32 mask) -{ - int rc; - - /* Do the right thing if hotkey_init has not been called yet */ - if (!tp_features.hotkey) { - hotkey_driver_mask = mask; - return 0; - } - - mutex_lock(&hotkey_mutex); - - HOTKEY_CONFIG_CRITICAL_START - hotkey_driver_mask = mask; -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - hotkey_source_mask |= (mask & ~hotkey_all_mask); -#endif - HOTKEY_CONFIG_CRITICAL_END - - rc = hotkey_mask_set((hotkey_acpi_mask | hotkey_driver_mask) & - ~hotkey_source_mask); - hotkey_poll_setup(true); - - mutex_unlock(&hotkey_mutex); - - return rc; -} - -static int hotkey_status_get(int *status) -{ - if (!acpi_evalf(hkey_handle, status, "DHKC", "d")) - return -EIO; - - return 0; -} - -static int hotkey_status_set(bool enable) -{ - if (!acpi_evalf(hkey_handle, NULL, "MHKC", "vd", enable ? 1 : 0)) - return -EIO; - - return 0; -} - -static void tpacpi_input_send_tabletsw(void) -{ - int state; - - if (tp_features.hotkey_tablet && - !hotkey_get_tablet_mode(&state)) { - mutex_lock(&tpacpi_inputdev_send_mutex); - - input_report_switch(tpacpi_inputdev, - SW_TABLET_MODE, !!state); - input_sync(tpacpi_inputdev); - - mutex_unlock(&tpacpi_inputdev_send_mutex); - } -} - -#define GCES_NO_SHUTTER_DEVICE BIT(31) - -static int get_camera_shutter(void) -{ - acpi_handle gces_handle; - int output; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GCES", &gces_handle))) - return -ENODEV; - - if (!acpi_evalf(gces_handle, &output, NULL, "dd", 0)) - return -EIO; - - if (output & GCES_NO_SHUTTER_DEVICE) - return -ENODEV; - - return output; -} - -static bool tpacpi_input_send_key(const u32 hkey, bool *send_acpi_ev) -{ - bool known_ev; - u32 scancode; - - if (tpacpi_driver_event(hkey)) - return true; - - /* - * Before the conversion to using the sparse-keymap helpers the driver used to - * map the hkey event codes to 0x00 - 0x4d scancodes so that a straight scancode - * indexed array could be used to map scancodes to keycodes: - * - * 0x1001 - 0x1020 -> 0x00 - 0x1f (Original ThinkPad events) - * 0x1103 - 0x1116 -> 0x20 - 0x33 (Adaptive keyboard, 2014 X1 Carbon) - * 0x1300 - 0x1319 -> 0x34 - 0x4d (Additional keys send in 2017+ models) - * - * The sparse-keymap tables still use these scancodes for these ranges to - * preserve userspace API compatibility (e.g. hwdb keymappings). - */ - if (hkey >= TP_HKEY_EV_ORIG_KEY_START && - hkey <= TP_HKEY_EV_ORIG_KEY_END) { - scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; - if (!(hotkey_user_mask & (1 << scancode))) - return true; /* Not reported but still a known code */ - } else if (hkey >= TP_HKEY_EV_ADAPTIVE_KEY_START && - hkey <= TP_HKEY_EV_ADAPTIVE_KEY_END) { - scancode = hkey - TP_HKEY_EV_ADAPTIVE_KEY_START + - TP_ACPI_HOTKEYSCAN_ADAPTIVE_START; - } else if (hkey >= TP_HKEY_EV_EXTENDED_KEY_START && - hkey <= TP_HKEY_EV_EXTENDED_KEY_END) { - scancode = hkey - TP_HKEY_EV_EXTENDED_KEY_START + - TP_ACPI_HOTKEYSCAN_EXTENDED_START; - } else { - /* - * Do not send ACPI netlink events for unknown hotkeys, to - * avoid userspace starting to rely on them. Instead these - * should be added to the keymap to send evdev events. - */ - if (send_acpi_ev) - *send_acpi_ev = false; - - scancode = hkey; - } - - mutex_lock(&tpacpi_inputdev_send_mutex); - known_ev = sparse_keymap_report_event(tpacpi_inputdev, scancode, 1, true); - mutex_unlock(&tpacpi_inputdev_send_mutex); - - return known_ev; -} - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL -static struct tp_acpi_drv_struct ibm_hotkey_acpidriver; - -/* Do NOT call without validating scancode first */ -static void tpacpi_hotkey_send_key(unsigned int scancode) -{ - tpacpi_input_send_key(TP_HKEY_EV_ORIG_KEY_START + scancode, NULL); -} - -static void hotkey_read_nvram(struct tp_nvram_state *n, const u32 m) -{ - u8 d; - - if (m & TP_NVRAM_HKEY_GROUP_HK2) { - d = nvram_read_byte(TP_NVRAM_ADDR_HK2); - n->thinkpad_toggle = !!(d & TP_NVRAM_MASK_HKT_THINKPAD); - n->zoom_toggle = !!(d & TP_NVRAM_MASK_HKT_ZOOM); - n->display_toggle = !!(d & TP_NVRAM_MASK_HKT_DISPLAY); - n->hibernate_toggle = !!(d & TP_NVRAM_MASK_HKT_HIBERNATE); - } - if (m & TP_ACPI_HKEY_KBD_LIGHT_MASK) { - d = nvram_read_byte(TP_NVRAM_ADDR_THINKLIGHT); - n->thinklight_toggle = !!(d & TP_NVRAM_MASK_THINKLIGHT); - } - if (m & TP_ACPI_HKEY_DISPXPAND_MASK) { - d = nvram_read_byte(TP_NVRAM_ADDR_VIDEO); - n->displayexp_toggle = - !!(d & TP_NVRAM_MASK_HKT_DISPEXPND); - } - if (m & TP_NVRAM_HKEY_GROUP_BRIGHTNESS) { - d = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); - n->brightness_level = (d & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) - >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; - n->brightness_toggle = - !!(d & TP_NVRAM_MASK_HKT_BRIGHTNESS); - } - if (m & TP_NVRAM_HKEY_GROUP_VOLUME) { - d = nvram_read_byte(TP_NVRAM_ADDR_MIXER); - n->volume_level = (d & TP_NVRAM_MASK_LEVEL_VOLUME) - >> TP_NVRAM_POS_LEVEL_VOLUME; - n->mute = !!(d & TP_NVRAM_MASK_MUTE); - n->volume_toggle = !!(d & TP_NVRAM_MASK_HKT_VOLUME); - } -} - -#define TPACPI_COMPARE_KEY(__scancode, __member) \ -do { \ - if ((event_mask & (1 << __scancode)) && \ - oldn->__member != newn->__member) \ - tpacpi_hotkey_send_key(__scancode); \ -} while (0) - -#define TPACPI_MAY_SEND_KEY(__scancode) \ -do { \ - if (event_mask & (1 << __scancode)) \ - tpacpi_hotkey_send_key(__scancode); \ -} while (0) - -static void issue_volchange(const unsigned int oldvol, - const unsigned int newvol, - const u32 event_mask) -{ - unsigned int i = oldvol; - - while (i > newvol) { - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); - i--; - } - while (i < newvol) { - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); - i++; - } -} - -static void issue_brightnesschange(const unsigned int oldbrt, - const unsigned int newbrt, - const u32 event_mask) -{ - unsigned int i = oldbrt; - - while (i > newbrt) { - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); - i--; - } - while (i < newbrt) { - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); - i++; - } -} - -static void hotkey_compare_and_issue_event(struct tp_nvram_state *oldn, - struct tp_nvram_state *newn, - const u32 event_mask) -{ - - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_THINKPAD, thinkpad_toggle); - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNSPACE, zoom_toggle); - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF7, display_toggle); - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF12, hibernate_toggle); - - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNPAGEUP, thinklight_toggle); - - TPACPI_COMPARE_KEY(TP_ACPI_HOTKEYSCAN_FNF8, displayexp_toggle); - - /* - * Handle volume - * - * This code is supposed to duplicate the IBM firmware behaviour: - * - Pressing MUTE issues mute hotkey message, even when already mute - * - Pressing Volume up/down issues volume up/down hotkey messages, - * even when already at maximum or minimum volume - * - The act of unmuting issues volume up/down notification, - * depending which key was used to unmute - * - * We are constrained to what the NVRAM can tell us, which is not much - * and certainly not enough if more than one volume hotkey was pressed - * since the last poll cycle. - * - * Just to make our life interesting, some newer Lenovo ThinkPads have - * bugs in the BIOS and may fail to update volume_toggle properly. - */ - if (newn->mute) { - /* muted */ - if (!oldn->mute || - oldn->volume_toggle != newn->volume_toggle || - oldn->volume_level != newn->volume_level) { - /* recently muted, or repeated mute keypress, or - * multiple presses ending in mute */ - issue_volchange(oldn->volume_level, newn->volume_level, - event_mask); - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_MUTE); - } - } else { - /* unmute */ - if (oldn->mute) { - /* recently unmuted, issue 'unmute' keypress */ - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); - } - if (oldn->volume_level != newn->volume_level) { - issue_volchange(oldn->volume_level, newn->volume_level, - event_mask); - } else if (oldn->volume_toggle != newn->volume_toggle) { - /* repeated vol up/down keypress at end of scale ? */ - if (newn->volume_level == 0) - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEDOWN); - else if (newn->volume_level >= TP_NVRAM_LEVEL_VOLUME_MAX) - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_VOLUMEUP); - } - } - - /* handle brightness */ - if (oldn->brightness_level != newn->brightness_level) { - issue_brightnesschange(oldn->brightness_level, - newn->brightness_level, event_mask); - } else if (oldn->brightness_toggle != newn->brightness_toggle) { - /* repeated key presses that didn't change state */ - if (newn->brightness_level == 0) - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNEND); - else if (newn->brightness_level >= bright_maxlvl - && !tp_features.bright_unkfw) - TPACPI_MAY_SEND_KEY(TP_ACPI_HOTKEYSCAN_FNHOME); - } - -#undef TPACPI_COMPARE_KEY -#undef TPACPI_MAY_SEND_KEY -} - -/* - * Polling driver - * - * We track all events in hotkey_source_mask all the time, since - * most of them are edge-based. We only issue those requested by - * hotkey_user_mask or hotkey_driver_mask, though. - */ -static int hotkey_kthread(void *data) -{ - struct tp_nvram_state s[2] = { 0 }; - u32 poll_mask, event_mask; - unsigned int si, so; - unsigned long t; - unsigned int change_detector; - unsigned int poll_freq; - bool was_frozen; - - if (tpacpi_lifecycle == TPACPI_LIFE_EXITING) - goto exit; - - set_freezable(); - - so = 0; - si = 1; - t = 0; - - /* Initial state for compares */ - mutex_lock(&hotkey_thread_data_mutex); - change_detector = hotkey_config_change; - poll_mask = hotkey_source_mask; - event_mask = hotkey_source_mask & - (hotkey_driver_mask | hotkey_user_mask); - poll_freq = hotkey_poll_freq; - mutex_unlock(&hotkey_thread_data_mutex); - hotkey_read_nvram(&s[so], poll_mask); - - while (!kthread_should_stop()) { - if (t == 0) { - if (likely(poll_freq)) - t = 1000/poll_freq; - else - t = 100; /* should never happen... */ - } - t = msleep_interruptible(t); - if (unlikely(kthread_freezable_should_stop(&was_frozen))) - break; - - if (t > 0 && !was_frozen) - continue; - - mutex_lock(&hotkey_thread_data_mutex); - if (was_frozen || hotkey_config_change != change_detector) { - /* forget old state on thaw or config change */ - si = so; - t = 0; - change_detector = hotkey_config_change; - } - poll_mask = hotkey_source_mask; - event_mask = hotkey_source_mask & - (hotkey_driver_mask | hotkey_user_mask); - poll_freq = hotkey_poll_freq; - mutex_unlock(&hotkey_thread_data_mutex); - - if (likely(poll_mask)) { - hotkey_read_nvram(&s[si], poll_mask); - if (likely(si != so)) { - hotkey_compare_and_issue_event(&s[so], &s[si], - event_mask); - } - } - - so = si; - si ^= 1; - } - -exit: - return 0; -} - -static void hotkey_poll_stop_sync(void) -{ - lockdep_assert_held(&hotkey_mutex); - - if (tpacpi_hotkey_task) { - kthread_stop(tpacpi_hotkey_task); - tpacpi_hotkey_task = NULL; - } -} - -static void hotkey_poll_setup(const bool may_warn) -{ - const u32 poll_driver_mask = hotkey_driver_mask & hotkey_source_mask; - const u32 poll_user_mask = hotkey_user_mask & hotkey_source_mask; - - lockdep_assert_held(&hotkey_mutex); - - if (hotkey_poll_freq > 0 && - (poll_driver_mask || - (poll_user_mask && tpacpi_inputdev->users > 0))) { - if (!tpacpi_hotkey_task) { - tpacpi_hotkey_task = kthread_run(hotkey_kthread, - NULL, TPACPI_NVRAM_KTHREAD_NAME); - if (IS_ERR(tpacpi_hotkey_task)) { - tpacpi_hotkey_task = NULL; - pr_err("could not create kernel thread for hotkey polling\n"); - } - } - } else { - hotkey_poll_stop_sync(); - if (may_warn && (poll_driver_mask || poll_user_mask) && - hotkey_poll_freq == 0) { - pr_notice("hot keys 0x%08x and/or events 0x%08x require polling, which is currently disabled\n", - poll_user_mask, poll_driver_mask); - } - } -} - -static void hotkey_poll_setup_safe(const bool may_warn) -{ - mutex_lock(&hotkey_mutex); - hotkey_poll_setup(may_warn); - mutex_unlock(&hotkey_mutex); -} - -static void hotkey_poll_set_freq(unsigned int freq) -{ - lockdep_assert_held(&hotkey_mutex); - - if (!freq) - hotkey_poll_stop_sync(); - - hotkey_poll_freq = freq; -} - -#else /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ - -static void hotkey_poll_setup(const bool __unused) -{ -} - -static void hotkey_poll_setup_safe(const bool __unused) -{ -} - -static void hotkey_poll_stop_sync(void) -{ -} -#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ - -static int hotkey_inputdev_open(struct input_dev *dev) -{ - switch (tpacpi_lifecycle) { - case TPACPI_LIFE_INIT: - case TPACPI_LIFE_RUNNING: - hotkey_poll_setup_safe(false); - return 0; - case TPACPI_LIFE_EXITING: - return -EBUSY; - } - - /* Should only happen if tpacpi_lifecycle is corrupt */ - BUG(); - return -EBUSY; -} - -static void hotkey_inputdev_close(struct input_dev *dev) -{ - /* disable hotkey polling when possible */ - if (tpacpi_lifecycle != TPACPI_LIFE_EXITING && - !(hotkey_source_mask & hotkey_driver_mask)) - hotkey_poll_setup_safe(false); -} - -/* sysfs hotkey enable ------------------------------------------------- */ -static ssize_t hotkey_enable_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res, status; - - printk_deprecated_attribute("hotkey_enable", - "Hotkey reporting is always enabled"); - - res = hotkey_status_get(&status); - if (res) - return res; - - return sysfs_emit(buf, "%d\n", status); -} - -static ssize_t hotkey_enable_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - - printk_deprecated_attribute("hotkey_enable", - "Hotkeys can be disabled through hotkey_mask"); - - if (parse_strtoul(buf, 1, &t)) - return -EINVAL; - - if (t == 0) - return -EPERM; - - return count; -} - -static DEVICE_ATTR_RW(hotkey_enable); - -/* sysfs hotkey mask --------------------------------------------------- */ -static ssize_t hotkey_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", hotkey_user_mask); -} - -static ssize_t hotkey_mask_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - int res; - - if (parse_strtoul(buf, 0xffffffffUL, &t)) - return -EINVAL; - - if (mutex_lock_killable(&hotkey_mutex)) - return -ERESTARTSYS; - - res = hotkey_user_mask_set(t); - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - hotkey_poll_setup(true); -#endif - - mutex_unlock(&hotkey_mutex); - - tpacpi_disclose_usertask("hotkey_mask", "set to 0x%08lx\n", t); - - return (res) ? res : count; -} - -static DEVICE_ATTR_RW(hotkey_mask); - -/* sysfs hotkey bios_enabled ------------------------------------------- */ -static ssize_t hotkey_bios_enabled_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0\n"); -} - -static DEVICE_ATTR_RO(hotkey_bios_enabled); - -/* sysfs hotkey bios_mask ---------------------------------------------- */ -static ssize_t hotkey_bios_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - printk_deprecated_attribute("hotkey_bios_mask", - "This attribute is useless."); - return sysfs_emit(buf, "0x%08x\n", hotkey_orig_mask); -} - -static DEVICE_ATTR_RO(hotkey_bios_mask); - -/* sysfs hotkey all_mask ----------------------------------------------- */ -static ssize_t hotkey_all_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", - hotkey_all_mask | hotkey_source_mask); -} - -static DEVICE_ATTR_RO(hotkey_all_mask); - -/* sysfs hotkey all_mask ----------------------------------------------- */ -static ssize_t hotkey_adaptive_all_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", - hotkey_adaptive_all_mask | hotkey_source_mask); -} - -static DEVICE_ATTR_RO(hotkey_adaptive_all_mask); - -/* sysfs hotkey recommended_mask --------------------------------------- */ -static ssize_t hotkey_recommended_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", - (hotkey_all_mask | hotkey_source_mask) - & ~hotkey_reserved_mask); -} - -static DEVICE_ATTR_RO(hotkey_recommended_mask); - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - -/* sysfs hotkey hotkey_source_mask ------------------------------------- */ -static ssize_t hotkey_source_mask_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "0x%08x\n", hotkey_source_mask); -} - -static ssize_t hotkey_source_mask_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - u32 r_ev; - int rc; - - if (parse_strtoul(buf, 0xffffffffUL, &t) || - ((t & ~TPACPI_HKEY_NVRAM_KNOWN_MASK) != 0)) - return -EINVAL; - - if (mutex_lock_killable(&hotkey_mutex)) - return -ERESTARTSYS; - - HOTKEY_CONFIG_CRITICAL_START - hotkey_source_mask = t; - HOTKEY_CONFIG_CRITICAL_END - - rc = hotkey_mask_set((hotkey_user_mask | hotkey_driver_mask) & - ~hotkey_source_mask); - hotkey_poll_setup(true); - - /* check if events needed by the driver got disabled */ - r_ev = hotkey_driver_mask & ~(hotkey_acpi_mask & hotkey_all_mask) - & ~hotkey_source_mask & TPACPI_HKEY_NVRAM_KNOWN_MASK; - - mutex_unlock(&hotkey_mutex); - - if (rc < 0) - pr_err("hotkey_source_mask: failed to update the firmware event mask!\n"); - - if (r_ev) - pr_notice("hotkey_source_mask: some important events were disabled: 0x%04x\n", - r_ev); - - tpacpi_disclose_usertask("hotkey_source_mask", "set to 0x%08lx\n", t); - - return (rc < 0) ? rc : count; -} - -static DEVICE_ATTR_RW(hotkey_source_mask); - -/* sysfs hotkey hotkey_poll_freq --------------------------------------- */ -static ssize_t hotkey_poll_freq_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "%d\n", hotkey_poll_freq); -} - -static ssize_t hotkey_poll_freq_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 25, &t)) - return -EINVAL; - - if (mutex_lock_killable(&hotkey_mutex)) - return -ERESTARTSYS; - - hotkey_poll_set_freq(t); - hotkey_poll_setup(true); - - mutex_unlock(&hotkey_mutex); - - tpacpi_disclose_usertask("hotkey_poll_freq", "set to %lu\n", t); - - return count; -} - -static DEVICE_ATTR_RW(hotkey_poll_freq); - -#endif /* CONFIG_THINKPAD_ACPI_HOTKEY_POLL */ - -/* sysfs hotkey radio_sw (pollable) ------------------------------------ */ -static ssize_t hotkey_radio_sw_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res; - res = hotkey_get_wlsw(); - if (res < 0) - return res; - - /* Opportunistic update */ - tpacpi_rfk_update_hwblock_state((res == TPACPI_RFK_RADIO_OFF)); - - return sysfs_emit(buf, "%d\n", - (res == TPACPI_RFK_RADIO_OFF) ? 0 : 1); -} - -static DEVICE_ATTR_RO(hotkey_radio_sw); - -static void hotkey_radio_sw_notify_change(void) -{ - if (tp_features.hotkey_wlsw) - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, - "hotkey_radio_sw"); -} - -/* sysfs hotkey tablet mode (pollable) --------------------------------- */ -static ssize_t hotkey_tablet_mode_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res, s; - res = hotkey_get_tablet_mode(&s); - if (res < 0) - return res; - - return sysfs_emit(buf, "%d\n", !!s); -} - -static DEVICE_ATTR_RO(hotkey_tablet_mode); - -static void hotkey_tablet_mode_notify_change(void) -{ - if (tp_features.hotkey_tablet) - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, - "hotkey_tablet_mode"); -} - -/* sysfs wakeup reason (pollable) -------------------------------------- */ -static ssize_t hotkey_wakeup_reason_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "%d\n", hotkey_wakeup_reason); -} - -static DEVICE_ATTR(wakeup_reason, S_IRUGO, hotkey_wakeup_reason_show, NULL); - -static void hotkey_wakeup_reason_notify_change(void) -{ - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, - "wakeup_reason"); -} - -/* sysfs wakeup hotunplug_complete (pollable) -------------------------- */ -static ssize_t hotkey_wakeup_hotunplug_complete_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "%d\n", hotkey_autosleep_ack); -} - -static DEVICE_ATTR(wakeup_hotunplug_complete, S_IRUGO, - hotkey_wakeup_hotunplug_complete_show, NULL); - -static void hotkey_wakeup_hotunplug_complete_notify_change(void) -{ - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, - "wakeup_hotunplug_complete"); -} - -/* sysfs adaptive kbd mode --------------------------------------------- */ - -static int adaptive_keyboard_get_mode(void); -static int adaptive_keyboard_set_mode(int new_mode); - -enum ADAPTIVE_KEY_MODE { - HOME_MODE, - WEB_BROWSER_MODE, - WEB_CONFERENCE_MODE, - FUNCTION_MODE, - LAYFLAT_MODE -}; - -static ssize_t adaptive_kbd_mode_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int current_mode; - - current_mode = adaptive_keyboard_get_mode(); - if (current_mode < 0) - return current_mode; - - return sysfs_emit(buf, "%d\n", current_mode); -} - -static ssize_t adaptive_kbd_mode_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - int res; - - if (parse_strtoul(buf, LAYFLAT_MODE, &t)) - return -EINVAL; - - res = adaptive_keyboard_set_mode(t); - return (res < 0) ? res : count; -} - -static DEVICE_ATTR_RW(adaptive_kbd_mode); - -static struct attribute *adaptive_kbd_attributes[] = { - &dev_attr_adaptive_kbd_mode.attr, - NULL -}; - -static umode_t hadaptive_kbd_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return tp_features.has_adaptive_kbd ? attr->mode : 0; -} - -static const struct attribute_group adaptive_kbd_attr_group = { - .is_visible = hadaptive_kbd_attr_is_visible, - .attrs = adaptive_kbd_attributes, -}; - -/* --------------------------------------------------------------------- */ - -static struct attribute *hotkey_attributes[] = { - &dev_attr_hotkey_enable.attr, - &dev_attr_hotkey_bios_enabled.attr, - &dev_attr_hotkey_bios_mask.attr, - &dev_attr_wakeup_reason.attr, - &dev_attr_wakeup_hotunplug_complete.attr, - &dev_attr_hotkey_mask.attr, - &dev_attr_hotkey_all_mask.attr, - &dev_attr_hotkey_adaptive_all_mask.attr, - &dev_attr_hotkey_recommended_mask.attr, - &dev_attr_hotkey_tablet_mode.attr, - &dev_attr_hotkey_radio_sw.attr, -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - &dev_attr_hotkey_source_mask.attr, - &dev_attr_hotkey_poll_freq.attr, -#endif - NULL -}; - -static umode_t hotkey_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - if (attr == &dev_attr_hotkey_tablet_mode.attr) { - if (!tp_features.hotkey_tablet) - return 0; - } else if (attr == &dev_attr_hotkey_radio_sw.attr) { - if (!tp_features.hotkey_wlsw) - return 0; - } - - return attr->mode; -} - -static const struct attribute_group hotkey_attr_group = { - .is_visible = hotkey_attr_is_visible, - .attrs = hotkey_attributes, -}; - -/* - * Sync both the hw and sw blocking state of all switches - */ -static void tpacpi_send_radiosw_update(void) -{ - int wlsw; - - /* - * We must sync all rfkill controllers *before* issuing any - * rfkill input events, or we will race the rfkill core input - * handler. - * - * tpacpi_inputdev_send_mutex works as a synchronization point - * for the above. - * - * We optimize to avoid numerous calls to hotkey_get_wlsw. - */ - - wlsw = hotkey_get_wlsw(); - - /* Sync hw blocking state first if it is hw-blocked */ - if (wlsw == TPACPI_RFK_RADIO_OFF) - tpacpi_rfk_update_hwblock_state(true); - - /* Sync hw blocking state last if it is hw-unblocked */ - if (wlsw == TPACPI_RFK_RADIO_ON) - tpacpi_rfk_update_hwblock_state(false); - - /* Issue rfkill input event for WLSW switch */ - if (!(wlsw < 0)) { - mutex_lock(&tpacpi_inputdev_send_mutex); - - input_report_switch(tpacpi_inputdev, - SW_RFKILL_ALL, (wlsw > 0)); - input_sync(tpacpi_inputdev); - - mutex_unlock(&tpacpi_inputdev_send_mutex); - } - - /* - * this can be unconditional, as we will poll state again - * if userspace uses the notify to read data - */ - hotkey_radio_sw_notify_change(); -} - -static void hotkey_exit(void) -{ - mutex_lock(&hotkey_mutex); - hotkey_poll_stop_sync(); - dbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_HKEY, - "restoring original HKEY status and mask\n"); - /* yes, there is a bitwise or below, we want the - * functions to be called even if one of them fail */ - if (((tp_features.hotkey_mask && - hotkey_mask_set(hotkey_orig_mask)) | - hotkey_status_set(false)) != 0) - pr_err("failed to restore hot key mask to BIOS defaults\n"); - - mutex_unlock(&hotkey_mutex); -} - -/* - * HKEY quirks: - * TPACPI_HK_Q_INIMASK: Supports FN+F3,FN+F4,FN+F12 - */ - -#define TPACPI_HK_Q_INIMASK 0x0001 - -static const struct tpacpi_quirk tpacpi_hotkey_qtable[] __initconst = { - TPACPI_Q_IBM('I', 'H', TPACPI_HK_Q_INIMASK), /* 600E */ - TPACPI_Q_IBM('I', 'N', TPACPI_HK_Q_INIMASK), /* 600E */ - TPACPI_Q_IBM('I', 'D', TPACPI_HK_Q_INIMASK), /* 770, 770E, 770ED */ - TPACPI_Q_IBM('I', 'W', TPACPI_HK_Q_INIMASK), /* A20m */ - TPACPI_Q_IBM('I', 'V', TPACPI_HK_Q_INIMASK), /* A20p */ - TPACPI_Q_IBM('1', '0', TPACPI_HK_Q_INIMASK), /* A21e, A22e */ - TPACPI_Q_IBM('K', 'U', TPACPI_HK_Q_INIMASK), /* A21e */ - TPACPI_Q_IBM('K', 'X', TPACPI_HK_Q_INIMASK), /* A21m, A22m */ - TPACPI_Q_IBM('K', 'Y', TPACPI_HK_Q_INIMASK), /* A21p, A22p */ - TPACPI_Q_IBM('1', 'B', TPACPI_HK_Q_INIMASK), /* A22e */ - TPACPI_Q_IBM('1', '3', TPACPI_HK_Q_INIMASK), /* A22m */ - TPACPI_Q_IBM('1', 'E', TPACPI_HK_Q_INIMASK), /* A30/p (0) */ - TPACPI_Q_IBM('1', 'C', TPACPI_HK_Q_INIMASK), /* R30 */ - TPACPI_Q_IBM('1', 'F', TPACPI_HK_Q_INIMASK), /* R31 */ - TPACPI_Q_IBM('I', 'Y', TPACPI_HK_Q_INIMASK), /* T20 */ - TPACPI_Q_IBM('K', 'Z', TPACPI_HK_Q_INIMASK), /* T21 */ - TPACPI_Q_IBM('1', '6', TPACPI_HK_Q_INIMASK), /* T22 */ - TPACPI_Q_IBM('I', 'Z', TPACPI_HK_Q_INIMASK), /* X20, X21 */ - TPACPI_Q_IBM('1', 'D', TPACPI_HK_Q_INIMASK), /* X22, X23, X24 */ -}; - -static int hotkey_init_tablet_mode(void) -{ - int in_tablet_mode = 0, res; - char *type = NULL; - - if (acpi_evalf(hkey_handle, &res, "GMMS", "qdd", 0)) { - int has_tablet_mode; - - in_tablet_mode = hotkey_gmms_get_tablet_mode(res, - &has_tablet_mode); - /* - * The Yoga 11e series has 2 accelerometers described by a - * BOSC0200 ACPI node. This setup relies on a Windows service - * which calls special ACPI methods on this node to report - * the laptop/tent/tablet mode to the EC. The bmc150 iio driver - * does not support this, so skip the hotkey on these models. - */ - if (has_tablet_mode && !dual_accel_detect()) - tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_GMMS; - type = "GMMS"; - } else if (acpi_evalf(hkey_handle, &res, "MHKG", "qd")) { - /* For X41t, X60t, X61t Tablets... */ - tp_features.hotkey_tablet = TP_HOTKEY_TABLET_USES_MHKG; - in_tablet_mode = !!(res & TP_HOTKEY_TABLET_MASK); - type = "MHKG"; - } - - if (!tp_features.hotkey_tablet) - return 0; - - pr_info("Tablet mode switch found (type: %s), currently in %s mode\n", - type, in_tablet_mode ? "tablet" : "laptop"); - - return in_tablet_mode; -} - -static const struct key_entry keymap_ibm[] __initconst = { - /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_BATTERY } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_COFFEE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_FN_F6 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, - /* Brightness: firmware always reacts, suppressed through hotkey_reserved_mask. */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, - /* Thinklight: firmware always reacts, suppressed through hotkey_reserved_mask. */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, - /* - * Volume: firmware always reacts and reprograms the built-in *extra* mixer. - * Suppressed by default through hotkey_reserved_mask. - */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, - { KE_END } -}; - -static const struct key_entry keymap_lenovo[] __initconst = { - /* Original hotkey mappings translated scancodes 0x00 - 0x1f */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF1, { KEY_FN_F1 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF2, { KEY_COFFEE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF3, { KEY_BATTERY } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF4, { KEY_SLEEP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF5, { KEY_WLAN } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF6, { KEY_CAMERA, } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF7, { KEY_SWITCHVIDEOMODE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF8, { KEY_FN_F8 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF9, { KEY_FN_F9 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF10, { KEY_FN_F10 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF11, { KEY_FN_F11 } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNF12, { KEY_SUSPEND } }, - /* - * These should be enabled --only-- when ACPI video is disabled and - * are handled in a special way by the init code. - */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNHOME, { KEY_BRIGHTNESSUP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNEND, { KEY_BRIGHTNESSDOWN } }, - /* Suppressed by default through hotkey_reserved_mask. */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNPAGEUP, { KEY_KBDILLUMTOGGLE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FNSPACE, { KEY_ZOOM } }, - /* - * Volume: z60/z61, T60 (BIOS version?): firmware always reacts and - * reprograms the built-in *extra* mixer. - * T60?, T61, R60?, R61: firmware and EC tries to send these over - * the regular keyboard (not through tpacpi). There are still weird bugs - * re. MUTE. May cause the BIOS to interfere with the HDA mixer. - * Suppressed by default through hotkey_reserved_mask. - */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEUP, { KEY_VOLUMEUP } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_VOLUMEDOWN, { KEY_VOLUMEDOWN } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE, { KEY_MUTE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_THINKPAD, { KEY_VENDOR } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MICMUTE, { KEY_MICMUTE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG, { KEY_CONFIG } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_SEARCH, { KEY_SEARCH } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_SCALE, { KEY_SCALE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_FILE, { KEY_FILE } }, - /* Adaptive keyboard mappings for Carbon X1 2014 translated scancodes 0x20 - 0x33 */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_MUTE2, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_BRIGHTNESS_ZERO, { KEY_BRIGHTNESS_MIN } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL, { KEY_SELECTIVE_SCREENSHOT } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CLOUD, { KEY_XFER } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK9, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_VOICE, { KEY_VOICECOMMAND } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK10, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_GESTURES, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK11, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK12, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_UNK13, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CONFIG2, { KEY_CONFIG } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_NEW_TAB, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_RELOAD, { KEY_REFRESH } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_BACK, { KEY_BACK } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_DOWN, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_UP, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_MIC_CANCELLATION, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CAMERA_MODE, { KEY_RESERVED } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_ROTATE_DISPLAY, { KEY_RESERVED } }, - /* Extended hotkeys mappings translated scancodes 0x34 - 0x4d */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_STAR, { KEY_BOOKMARKS } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CLIPPING_TOOL2, { KEY_SELECTIVE_SCREENSHOT } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_CALCULATOR, { KEY_CALC } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_BLUETOOTH, { KEY_BLUETOOTH } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_KEYBOARD, { KEY_KEYBOARD } }, - /* Used by "Lenovo Quick Clean" */ - { KE_KEY, TP_ACPI_HOTKEYSCAN_FN_RIGHT_SHIFT, { KEY_FN_RIGHT_SHIFT } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_NOTIFICATION_CENTER, { KEY_NOTIFICATION_CENTER } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_PICKUP_PHONE, { KEY_PICKUP_PHONE } }, - { KE_KEY, TP_ACPI_HOTKEYSCAN_HANGUP_PHONE, { KEY_HANGUP_PHONE } }, - /* - * All mapping below are for raw untranslated hkey event codes mapped directly - * after switching to sparse keymap support. The mappings above use translated - * scancodes to preserve uAPI compatibility, see tpacpi_input_send_key(). - */ - { KE_KEY, 0x131d, { KEY_VENDOR } }, /* System debug info, similar to old ThinkPad key */ - { KE_KEY, 0x1320, { KEY_LINK_PHONE } }, - { KE_KEY, TP_HKEY_EV_TRACK_DOUBLETAP /* 0x8036 */, { KEY_PROG4 } }, - { KE_END } -}; - -static int __init hotkey_init(struct ibm_init_struct *iibm) -{ - enum keymap_index { - TPACPI_KEYMAP_IBM_GENERIC = 0, - TPACPI_KEYMAP_LENOVO_GENERIC, - }; - - static const struct tpacpi_quirk tpacpi_keymap_qtable[] __initconst = { - /* Generic maps (fallback) */ - { - .vendor = PCI_VENDOR_ID_IBM, - .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, - .quirks = TPACPI_KEYMAP_IBM_GENERIC, - }, - { - .vendor = PCI_VENDOR_ID_LENOVO, - .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, - .quirks = TPACPI_KEYMAP_LENOVO_GENERIC, - }, - }; - - unsigned long keymap_id, quirks; - const struct key_entry *keymap; - bool radiosw_state = false; - bool tabletsw_state = false; - int hkeyv, res, status, camera_shutter_state; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "initializing hotkey subdriver\n"); - - BUG_ON(!tpacpi_inputdev); - BUG_ON(tpacpi_inputdev->open != NULL || - tpacpi_inputdev->close != NULL); - - TPACPI_ACPIHANDLE_INIT(hkey); - mutex_init(&hotkey_mutex); - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - mutex_init(&hotkey_thread_data_mutex); -#endif - - /* hotkey not supported on 570 */ - tp_features.hotkey = hkey_handle != NULL; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "hotkeys are %s\n", - str_supported(tp_features.hotkey)); - - if (!tp_features.hotkey) - return -ENODEV; - - quirks = tpacpi_check_quirks(tpacpi_hotkey_qtable, - ARRAY_SIZE(tpacpi_hotkey_qtable)); - - tpacpi_disable_brightness_delay(); - - /* mask not supported on 600e/x, 770e, 770x, A21e, A2xm/p, - A30, R30, R31, T20-22, X20-21, X22-24. Detected by checking - for HKEY interface version 0x100 */ - if (acpi_evalf(hkey_handle, &hkeyv, "MHKV", "qd")) { - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "firmware HKEY interface version: 0x%x\n", - hkeyv); - - switch (hkeyv >> 8) { - case 1: - /* - * MHKV 0x100 in A31, R40, R40e, - * T4x, X31, and later - */ - - /* Paranoia check AND init hotkey_all_mask */ - if (!acpi_evalf(hkey_handle, &hotkey_all_mask, - "MHKA", "qd")) { - pr_err("missing MHKA handler, please report this to %s\n", - TPACPI_MAIL); - /* Fallback: pre-init for FN+F3,F4,F12 */ - hotkey_all_mask = 0x080cU; - } else { - tp_features.hotkey_mask = 1; - } - break; - - case 2: - /* - * MHKV 0x200 in X1, T460s, X260, T560, X1 Tablet (2016) - */ - - /* Paranoia check AND init hotkey_all_mask */ - if (!acpi_evalf(hkey_handle, &hotkey_all_mask, - "MHKA", "dd", 1)) { - pr_err("missing MHKA handler, please report this to %s\n", - TPACPI_MAIL); - /* Fallback: pre-init for FN+F3,F4,F12 */ - hotkey_all_mask = 0x080cU; - } else { - tp_features.hotkey_mask = 1; - } - - /* - * Check if we have an adaptive keyboard, like on the - * Lenovo Carbon X1 2014 (2nd Gen). - */ - if (acpi_evalf(hkey_handle, &hotkey_adaptive_all_mask, - "MHKA", "dd", 2)) { - if (hotkey_adaptive_all_mask != 0) - tp_features.has_adaptive_kbd = true; - } else { - tp_features.has_adaptive_kbd = false; - hotkey_adaptive_all_mask = 0x0U; - } - break; - - default: - pr_err("unknown version of the HKEY interface: 0x%x\n", - hkeyv); - pr_err("please report this to %s\n", TPACPI_MAIL); - break; - } - } - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "hotkey masks are %s\n", - str_supported(tp_features.hotkey_mask)); - - /* Init hotkey_all_mask if not initialized yet */ - if (!tp_features.hotkey_mask && !hotkey_all_mask && - (quirks & TPACPI_HK_Q_INIMASK)) - hotkey_all_mask = 0x080cU; /* FN+F12, FN+F4, FN+F3 */ - - /* Init hotkey_acpi_mask and hotkey_orig_mask */ - if (tp_features.hotkey_mask) { - /* hotkey_source_mask *must* be zero for - * the first hotkey_mask_get to return hotkey_orig_mask */ - mutex_lock(&hotkey_mutex); - res = hotkey_mask_get(); - mutex_unlock(&hotkey_mutex); - if (res) - return res; - - hotkey_orig_mask = hotkey_acpi_mask; - } else { - hotkey_orig_mask = hotkey_all_mask; - hotkey_acpi_mask = hotkey_all_mask; - } - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_wlswemul) { - tp_features.hotkey_wlsw = 1; - radiosw_state = !!tpacpi_wlsw_emulstate; - pr_info("radio switch emulation enabled\n"); - } else -#endif - /* Not all thinkpads have a hardware radio switch */ - if (acpi_evalf(hkey_handle, &status, "WLSW", "qd")) { - tp_features.hotkey_wlsw = 1; - radiosw_state = !!status; - pr_info("radio switch found; radios are %s\n", str_enabled_disabled(status & BIT(0))); - } - - tabletsw_state = hotkey_init_tablet_mode(); - - /* Set up key map */ - keymap_id = tpacpi_check_quirks(tpacpi_keymap_qtable, - ARRAY_SIZE(tpacpi_keymap_qtable)); - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "using keymap number %lu\n", keymap_id); - - /* Keys which should be reserved on both IBM and Lenovo models */ - hotkey_reserved_mask = TP_ACPI_HKEY_KBD_LIGHT_MASK | - TP_ACPI_HKEY_VOLUP_MASK | - TP_ACPI_HKEY_VOLDWN_MASK | - TP_ACPI_HKEY_MUTE_MASK; - /* - * Reserve brightness up/down unconditionally on IBM models, on Lenovo - * models these are disabled based on acpi_video_get_backlight_type(). - */ - if (keymap_id == TPACPI_KEYMAP_IBM_GENERIC) { - hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | - TP_ACPI_HKEY_BRGHTDWN_MASK; - keymap = keymap_ibm; - } else { - keymap = keymap_lenovo; - } - - res = sparse_keymap_setup(tpacpi_inputdev, keymap, NULL); - if (res) - return res; - - camera_shutter_state = get_camera_shutter(); - if (camera_shutter_state >= 0) { - input_set_capability(tpacpi_inputdev, EV_SW, SW_CAMERA_LENS_COVER); - input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); - } - - if (tp_features.hotkey_wlsw) { - input_set_capability(tpacpi_inputdev, EV_SW, SW_RFKILL_ALL); - input_report_switch(tpacpi_inputdev, - SW_RFKILL_ALL, radiosw_state); - } - if (tp_features.hotkey_tablet) { - input_set_capability(tpacpi_inputdev, EV_SW, SW_TABLET_MODE); - input_report_switch(tpacpi_inputdev, - SW_TABLET_MODE, tabletsw_state); - } - - /* Do not issue duplicate brightness change events to - * userspace. tpacpi_detect_brightness_capabilities() must have - * been called before this point */ - if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { - pr_info("This ThinkPad has standard ACPI backlight brightness control, supported by the ACPI video driver\n"); - pr_notice("Disabling thinkpad-acpi brightness events by default...\n"); - - /* Disable brightness up/down on Lenovo thinkpads when - * ACPI is handling them, otherwise it is plain impossible - * for userspace to do something even remotely sane */ - hotkey_reserved_mask |= TP_ACPI_HKEY_BRGHTUP_MASK | - TP_ACPI_HKEY_BRGHTDWN_MASK; - } - -#ifdef CONFIG_THINKPAD_ACPI_HOTKEY_POLL - hotkey_source_mask = TPACPI_HKEY_NVRAM_GOOD_MASK - & ~hotkey_all_mask - & ~hotkey_reserved_mask; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "hotkey source mask 0x%08x, polling freq %u\n", - hotkey_source_mask, hotkey_poll_freq); -#endif - - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "enabling firmware HKEY event interface...\n"); - res = hotkey_status_set(true); - if (res) { - hotkey_exit(); - return res; - } - mutex_lock(&hotkey_mutex); - res = hotkey_mask_set(((hotkey_all_mask & ~hotkey_reserved_mask) - | hotkey_driver_mask) - & ~hotkey_source_mask); - mutex_unlock(&hotkey_mutex); - if (res < 0 && res != -ENXIO) { - hotkey_exit(); - return res; - } - hotkey_user_mask = (hotkey_acpi_mask | hotkey_source_mask) - & ~hotkey_reserved_mask; - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_HKEY, - "initial masks: user=0x%08x, fw=0x%08x, poll=0x%08x\n", - hotkey_user_mask, hotkey_acpi_mask, hotkey_source_mask); - - tpacpi_inputdev->open = &hotkey_inputdev_open; - tpacpi_inputdev->close = &hotkey_inputdev_close; - - hotkey_poll_setup_safe(true); - - /* Enable doubletap by default */ - tp_features.trackpoint_doubletap = 1; - - return 0; -} - -/* Thinkpad X1 Carbon support 5 modes including Home mode, Web browser - * mode, Web conference mode, Function mode and Lay-flat mode. - * We support Home mode and Function mode currently. - * - * Will consider support rest of modes in future. - * - */ -static const int adaptive_keyboard_modes[] = { - HOME_MODE, -/* WEB_BROWSER_MODE = 2, - WEB_CONFERENCE_MODE = 3, */ - FUNCTION_MODE -}; - -/* press Fn key a while second, it will switch to Function Mode. Then - * release Fn key, previous mode be restored. - */ -static bool adaptive_keyboard_mode_is_saved; -static int adaptive_keyboard_prev_mode; - -static int adaptive_keyboard_get_mode(void) -{ - int mode = 0; - - if (!acpi_evalf(hkey_handle, &mode, "GTRW", "dd", 0)) { - pr_err("Cannot read adaptive keyboard mode\n"); - return -EIO; - } - - return mode; -} - -static int adaptive_keyboard_set_mode(int new_mode) -{ - if (new_mode < 0 || - new_mode > LAYFLAT_MODE) - return -EINVAL; - - if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", new_mode)) { - pr_err("Cannot set adaptive keyboard mode\n"); - return -EIO; - } - - return 0; -} - -static int adaptive_keyboard_get_next_mode(int mode) -{ - size_t i; - size_t max_mode = ARRAY_SIZE(adaptive_keyboard_modes) - 1; - - for (i = 0; i <= max_mode; i++) { - if (adaptive_keyboard_modes[i] == mode) - break; - } - - if (i >= max_mode) - i = 0; - else - i++; - - return adaptive_keyboard_modes[i]; -} - -static void adaptive_keyboard_change_row(void) -{ - int mode; - - if (adaptive_keyboard_mode_is_saved) { - mode = adaptive_keyboard_prev_mode; - adaptive_keyboard_mode_is_saved = false; - } else { - mode = adaptive_keyboard_get_mode(); - if (mode < 0) - return; - mode = adaptive_keyboard_get_next_mode(mode); - } - - adaptive_keyboard_set_mode(mode); -} - -static void adaptive_keyboard_s_quickview_row(void) -{ - int mode; - - mode = adaptive_keyboard_get_mode(); - if (mode < 0) - return; - - adaptive_keyboard_prev_mode = mode; - adaptive_keyboard_mode_is_saved = true; - - adaptive_keyboard_set_mode(FUNCTION_MODE); -} - -/* 0x1000-0x1FFF: key presses */ -static bool hotkey_notify_hotkey(const u32 hkey, bool *send_acpi_ev) -{ - /* Never send ACPI netlink events for original hotkeys (hkey: 0x1001 - 0x1020) */ - if (hkey >= TP_HKEY_EV_ORIG_KEY_START && hkey <= TP_HKEY_EV_ORIG_KEY_END) { - *send_acpi_ev = false; - - /* Original hotkeys may be polled from NVRAM instead */ - unsigned int scancode = hkey - TP_HKEY_EV_ORIG_KEY_START; - if (hotkey_source_mask & (1 << scancode)) - return true; - } - - return tpacpi_input_send_key(hkey, send_acpi_ev); -} - -/* 0x2000-0x2FFF: Wakeup reason */ -static bool hotkey_notify_wakeup(const u32 hkey, bool *send_acpi_ev) -{ - switch (hkey) { - case TP_HKEY_EV_WKUP_S3_UNDOCK: /* suspend, undock */ - case TP_HKEY_EV_WKUP_S4_UNDOCK: /* hibernation, undock */ - hotkey_wakeup_reason = TP_ACPI_WAKEUP_UNDOCK; - *send_acpi_ev = false; - break; - - case TP_HKEY_EV_WKUP_S3_BAYEJ: /* suspend, bay eject */ - case TP_HKEY_EV_WKUP_S4_BAYEJ: /* hibernation, bay eject */ - hotkey_wakeup_reason = TP_ACPI_WAKEUP_BAYEJ; - *send_acpi_ev = false; - break; - - case TP_HKEY_EV_WKUP_S3_BATLOW: /* Battery on critical low level/S3 */ - case TP_HKEY_EV_WKUP_S4_BATLOW: /* Battery on critical low level/S4 */ - pr_alert("EMERGENCY WAKEUP: battery almost empty\n"); - /* how to auto-heal: */ - /* 2313: woke up from S3, go to S4/S5 */ - /* 2413: woke up from S4, go to S5 */ - break; - - default: - return false; - } - - if (hotkey_wakeup_reason != TP_ACPI_WAKEUP_NONE) { - pr_info("woke up due to a hot-unplug request...\n"); - hotkey_wakeup_reason_notify_change(); - } - return true; -} - -/* 0x4000-0x4FFF: dock-related events */ -static bool hotkey_notify_dockevent(const u32 hkey, bool *send_acpi_ev) -{ - switch (hkey) { - case TP_HKEY_EV_UNDOCK_ACK: - /* ACPI undock operation completed after wakeup */ - hotkey_autosleep_ack = 1; - pr_info("undocked\n"); - hotkey_wakeup_hotunplug_complete_notify_change(); - return true; - - case TP_HKEY_EV_HOTPLUG_DOCK: /* docked to port replicator */ - pr_info("docked into hotplug port replicator\n"); - return true; - case TP_HKEY_EV_HOTPLUG_UNDOCK: /* undocked from port replicator */ - pr_info("undocked from hotplug port replicator\n"); - return true; - - /* - * Deliberately ignore attaching and detaching the keybord cover to avoid - * duplicates from intel-vbtn, which already emits SW_TABLET_MODE events - * to userspace. - * - * Please refer to the following thread for more information and a preliminary - * implementation using the GTOP ("Get Tablet OPtions") interface that could be - * extended to other attachment options of the ThinkPad X1 Tablet series, such as - * the Pico cartridge dock module: - * https://lore.kernel.org/platform-driver-x86/38cb8265-1e30-d547-9e12-b4ae290be737@a-kobel.de/ - */ - case TP_HKEY_EV_KBD_COVER_ATTACH: - case TP_HKEY_EV_KBD_COVER_DETACH: - *send_acpi_ev = false; - return true; - - default: - return false; - } -} - -/* 0x5000-0x5FFF: human interface helpers */ -static bool hotkey_notify_usrevent(const u32 hkey, bool *send_acpi_ev) -{ - switch (hkey) { - case TP_HKEY_EV_PEN_INSERTED: /* X61t: tablet pen inserted into bay */ - case TP_HKEY_EV_PEN_REMOVED: /* X61t: tablet pen removed from bay */ - return true; - - case TP_HKEY_EV_TABLET_TABLET: /* X41t-X61t: tablet mode */ - case TP_HKEY_EV_TABLET_NOTEBOOK: /* X41t-X61t: normal mode */ - tpacpi_input_send_tabletsw(); - hotkey_tablet_mode_notify_change(); - *send_acpi_ev = false; - return true; - - case TP_HKEY_EV_LID_CLOSE: /* Lid closed */ - case TP_HKEY_EV_LID_OPEN: /* Lid opened */ - case TP_HKEY_EV_BRGHT_CHANGED: /* brightness changed */ - /* do not propagate these events */ - *send_acpi_ev = false; - return true; - - default: - return false; - } -} - -static void thermal_dump_all_sensors(void); -static void palmsensor_refresh(void); - -/* 0x6000-0x6FFF: thermal alarms/notices and keyboard events */ -static bool hotkey_notify_6xxx(const u32 hkey, bool *send_acpi_ev) -{ - switch (hkey) { - case TP_HKEY_EV_THM_TABLE_CHANGED: - pr_debug("EC reports: Thermal Table has changed\n"); - /* recommended action: do nothing, we don't have - * Lenovo ATM information */ - return true; - case TP_HKEY_EV_THM_CSM_COMPLETED: - pr_debug("EC reports: Thermal Control Command set completed (DYTC)\n"); - /* Thermal event - pass on to event handler */ - tpacpi_driver_event(hkey); - return true; - case TP_HKEY_EV_THM_TRANSFM_CHANGED: - pr_debug("EC reports: Thermal Transformation changed (GMTS)\n"); - /* recommended action: do nothing, we don't have - * Lenovo ATM information */ - return true; - case TP_HKEY_EV_ALARM_BAT_HOT: - pr_crit("THERMAL ALARM: battery is too hot!\n"); - /* recommended action: warn user through gui */ - break; - case TP_HKEY_EV_ALARM_BAT_XHOT: - pr_alert("THERMAL EMERGENCY: battery is extremely hot!\n"); - /* recommended action: immediate sleep/hibernate */ - break; - case TP_HKEY_EV_ALARM_BAT_LIM_CHANGE: - pr_debug("Battery Info: battery charge threshold changed\n"); - /* User changed charging threshold. No action needed */ - return true; - case TP_HKEY_EV_ALARM_SENSOR_HOT: - pr_crit("THERMAL ALARM: a sensor reports something is too hot!\n"); - /* recommended action: warn user through gui, that */ - /* some internal component is too hot */ - break; - case TP_HKEY_EV_ALARM_SENSOR_XHOT: - pr_alert("THERMAL EMERGENCY: a sensor reports something is extremely hot!\n"); - /* recommended action: immediate sleep/hibernate */ - break; - case TP_HKEY_EV_AC_CHANGED: - /* X120e, X121e, X220, X220i, X220t, X230, T420, T420s, W520: - * AC status changed; can be triggered by plugging or - * unplugging AC adapter, docking or undocking. */ - - fallthrough; - - case TP_HKEY_EV_KEY_NUMLOCK: - case TP_HKEY_EV_KEY_FN: - /* key press events, we just ignore them as long as the EC - * is still reporting them in the normal keyboard stream */ - *send_acpi_ev = false; - return true; - - case TP_HKEY_EV_KEY_FN_ESC: - /* Get the media key status to force the status LED to update */ - acpi_evalf(hkey_handle, NULL, "GMKS", "v"); - *send_acpi_ev = false; - return true; - - case TP_HKEY_EV_TABLET_CHANGED: - tpacpi_input_send_tabletsw(); - hotkey_tablet_mode_notify_change(); - *send_acpi_ev = false; - return true; - - case TP_HKEY_EV_PALM_DETECTED: - case TP_HKEY_EV_PALM_UNDETECTED: - /* palm detected - pass on to event handler */ - palmsensor_refresh(); - return true; - - default: - /* report simply as unknown, no sensor dump */ - return false; - } - - thermal_dump_all_sensors(); - return true; -} - -static bool hotkey_notify_8xxx(const u32 hkey, bool *send_acpi_ev) -{ - switch (hkey) { - case TP_HKEY_EV_TRACK_DOUBLETAP: - if (tp_features.trackpoint_doubletap) - tpacpi_input_send_key(hkey, send_acpi_ev); - - return true; - default: - return false; - } -} - -static void hotkey_notify(struct ibm_struct *ibm, u32 event) -{ - u32 hkey; - bool send_acpi_ev; - bool known_ev; - - if (event != 0x80) { - pr_err("unknown HKEY notification event %d\n", event); - /* forward it to userspace, maybe it knows how to handle it */ - acpi_bus_generate_netlink_event( - ibm->acpi->device->pnp.device_class, - dev_name(&ibm->acpi->device->dev), - event, 0); - return; - } - - while (1) { - if (!acpi_evalf(hkey_handle, &hkey, "MHKP", "d")) { - pr_err("failed to retrieve HKEY event\n"); - return; - } - - if (hkey == 0) { - /* queue empty */ - return; - } - - send_acpi_ev = true; - known_ev = false; - - switch (hkey >> 12) { - case 1: - /* 0x1000-0x1FFF: key presses */ - known_ev = hotkey_notify_hotkey(hkey, &send_acpi_ev); - break; - case 2: - /* 0x2000-0x2FFF: Wakeup reason */ - known_ev = hotkey_notify_wakeup(hkey, &send_acpi_ev); - break; - case 3: - /* 0x3000-0x3FFF: bay-related wakeups */ - switch (hkey) { - case TP_HKEY_EV_BAYEJ_ACK: - hotkey_autosleep_ack = 1; - pr_info("bay ejected\n"); - hotkey_wakeup_hotunplug_complete_notify_change(); - known_ev = true; - break; - case TP_HKEY_EV_OPTDRV_EJ: - /* FIXME: kick libata if SATA link offline */ - known_ev = true; - break; - } - break; - case 4: - /* 0x4000-0x4FFF: dock-related events */ - known_ev = hotkey_notify_dockevent(hkey, &send_acpi_ev); - break; - case 5: - /* 0x5000-0x5FFF: human interface helpers */ - known_ev = hotkey_notify_usrevent(hkey, &send_acpi_ev); - break; - case 6: - /* 0x6000-0x6FFF: thermal alarms/notices and - * keyboard events */ - known_ev = hotkey_notify_6xxx(hkey, &send_acpi_ev); - break; - case 7: - /* 0x7000-0x7FFF: misc */ - if (tp_features.hotkey_wlsw && - hkey == TP_HKEY_EV_RFKILL_CHANGED) { - tpacpi_send_radiosw_update(); - send_acpi_ev = false; - known_ev = true; - } - break; - case 8: - /* 0x8000-0x8FFF: misc2 */ - known_ev = hotkey_notify_8xxx(hkey, &send_acpi_ev); - break; - } - if (!known_ev) { - pr_notice("unhandled HKEY event 0x%04x\n", hkey); - pr_notice("please report the conditions when this event happened to %s\n", - TPACPI_MAIL); - } - - /* netlink events */ - if (send_acpi_ev) { - acpi_bus_generate_netlink_event( - ibm->acpi->device->pnp.device_class, - dev_name(&ibm->acpi->device->dev), - event, hkey); - } - } -} - -static void hotkey_suspend(void) -{ - /* Do these on suspend, we get the events on early resume! */ - hotkey_wakeup_reason = TP_ACPI_WAKEUP_NONE; - hotkey_autosleep_ack = 0; - - /* save previous mode of adaptive keyboard of X1 Carbon */ - if (tp_features.has_adaptive_kbd) { - if (!acpi_evalf(hkey_handle, &adaptive_keyboard_prev_mode, - "GTRW", "dd", 0)) { - pr_err("Cannot read adaptive keyboard mode.\n"); - } - } -} - -static void hotkey_resume(void) -{ - tpacpi_disable_brightness_delay(); - - mutex_lock(&hotkey_mutex); - if (hotkey_status_set(true) < 0 || - hotkey_mask_set(hotkey_acpi_mask) < 0) - pr_err("error while attempting to reset the event firmware interface\n"); - mutex_unlock(&hotkey_mutex); - - tpacpi_send_radiosw_update(); - tpacpi_input_send_tabletsw(); - hotkey_tablet_mode_notify_change(); - hotkey_wakeup_reason_notify_change(); - hotkey_wakeup_hotunplug_complete_notify_change(); - hotkey_poll_setup_safe(false); - - /* restore previous mode of adapive keyboard of X1 Carbon */ - if (tp_features.has_adaptive_kbd) { - if (!acpi_evalf(hkey_handle, NULL, "STRW", "vd", - adaptive_keyboard_prev_mode)) { - pr_err("Cannot set adaptive keyboard mode.\n"); - } - } -} - -/* procfs -------------------------------------------------------------- */ -static int hotkey_read(struct seq_file *m) -{ - int res, status; - - if (!tp_features.hotkey) { - seq_printf(m, "status:\t\tnot supported\n"); - return 0; - } - - if (mutex_lock_killable(&hotkey_mutex)) - return -ERESTARTSYS; - res = hotkey_status_get(&status); - if (!res) - res = hotkey_mask_get(); - mutex_unlock(&hotkey_mutex); - if (res) - return res; - - seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status & BIT(0))); - if (hotkey_all_mask) { - seq_printf(m, "mask:\t\t0x%08x\n", hotkey_user_mask); - seq_printf(m, "commands:\tenable, disable, reset, \n"); - } else { - seq_printf(m, "mask:\t\tnot supported\n"); - seq_printf(m, "commands:\tenable, disable, reset\n"); - } - - return 0; -} - -static void hotkey_enabledisable_warn(bool enable) -{ - tpacpi_log_usertask("procfs hotkey enable/disable"); - if (!WARN((tpacpi_lifecycle == TPACPI_LIFE_RUNNING || !enable), - pr_fmt("hotkey enable/disable functionality has been removed from the driver. Hotkeys are always enabled.\n"))) - pr_err("Please remove the hotkey=enable module parameter, it is deprecated. Hotkeys are always enabled.\n"); -} - -static int hotkey_write(char *buf) -{ - int res; - u32 mask; - char *cmd; - - if (!tp_features.hotkey) - return -ENODEV; - - if (mutex_lock_killable(&hotkey_mutex)) - return -ERESTARTSYS; - - mask = hotkey_user_mask; - - res = 0; - while ((cmd = strsep(&buf, ","))) { - if (strstarts(cmd, "enable")) { - hotkey_enabledisable_warn(1); - } else if (strstarts(cmd, "disable")) { - hotkey_enabledisable_warn(0); - res = -EPERM; - } else if (strstarts(cmd, "reset")) { - mask = (hotkey_all_mask | hotkey_source_mask) - & ~hotkey_reserved_mask; - } else if (sscanf(cmd, "0x%x", &mask) == 1) { - /* mask set */ - } else if (sscanf(cmd, "%x", &mask) == 1) { - /* mask set */ - } else { - res = -EINVAL; - goto errexit; - } - } - - if (!res) { - tpacpi_disclose_usertask("procfs hotkey", - "set mask to 0x%08x\n", mask); - res = hotkey_user_mask_set(mask); - } - -errexit: - mutex_unlock(&hotkey_mutex); - return res; -} - -static const struct acpi_device_id ibm_htk_device_ids[] = { - {TPACPI_ACPI_IBM_HKEY_HID, 0}, - {TPACPI_ACPI_LENOVO_HKEY_HID, 0}, - {TPACPI_ACPI_LENOVO_HKEY_V2_HID, 0}, - {"", 0}, -}; - -static struct tp_acpi_drv_struct ibm_hotkey_acpidriver = { - .hid = ibm_htk_device_ids, - .notify = hotkey_notify, - .handle = &hkey_handle, - .type = ACPI_DEVICE_NOTIFY, -}; - -static struct ibm_struct hotkey_driver_data = { - .name = "hotkey", - .read = hotkey_read, - .write = hotkey_write, - .exit = hotkey_exit, - .resume = hotkey_resume, - .suspend = hotkey_suspend, - .acpi = &ibm_hotkey_acpidriver, -}; - -/************************************************************************* - * Bluetooth subdriver - */ - -enum { - /* ACPI GBDC/SBDC bits */ - TP_ACPI_BLUETOOTH_HWPRESENT = 0x01, /* Bluetooth hw available */ - TP_ACPI_BLUETOOTH_RADIOSSW = 0x02, /* Bluetooth radio enabled */ - TP_ACPI_BLUETOOTH_RESUMECTRL = 0x04, /* Bluetooth state at resume: - 0 = disable, 1 = enable */ -}; - -enum { - /* ACPI \BLTH commands */ - TP_ACPI_BLTH_GET_ULTRAPORT_ID = 0x00, /* Get Ultraport BT ID */ - TP_ACPI_BLTH_GET_PWR_ON_RESUME = 0x01, /* Get power-on-resume state */ - TP_ACPI_BLTH_PWR_ON_ON_RESUME = 0x02, /* Resume powered on */ - TP_ACPI_BLTH_PWR_OFF_ON_RESUME = 0x03, /* Resume powered off */ - TP_ACPI_BLTH_SAVE_STATE = 0x05, /* Save state for S4/S5 */ -}; - -#define TPACPI_RFK_BLUETOOTH_SW_NAME "tpacpi_bluetooth_sw" - -static int bluetooth_get_status(void) -{ - int status; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_bluetoothemul) - return (tpacpi_bluetooth_emulstate) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -#endif - - if (!acpi_evalf(hkey_handle, &status, "GBDC", "d")) - return -EIO; - - return ((status & TP_ACPI_BLUETOOTH_RADIOSSW) != 0) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -} - -static int bluetooth_set_status(enum tpacpi_rfkill_state state) -{ - int status; - - vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s bluetooth\n", - str_enable_disable(state == TPACPI_RFK_RADIO_ON)); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_bluetoothemul) { - tpacpi_bluetooth_emulstate = (state == TPACPI_RFK_RADIO_ON); - return 0; - } -#endif - - if (state == TPACPI_RFK_RADIO_ON) - status = TP_ACPI_BLUETOOTH_RADIOSSW - | TP_ACPI_BLUETOOTH_RESUMECTRL; - else - status = 0; - - if (!acpi_evalf(hkey_handle, NULL, "SBDC", "vd", status)) - return -EIO; - - return 0; -} - -/* sysfs bluetooth enable ---------------------------------------------- */ -static ssize_t bluetooth_enable_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_BLUETOOTH_SW_ID, - attr, buf); -} - -static ssize_t bluetooth_enable_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_BLUETOOTH_SW_ID, - attr, buf, count); -} - -static DEVICE_ATTR_RW(bluetooth_enable); - -/* --------------------------------------------------------------------- */ - -static struct attribute *bluetooth_attributes[] = { - &dev_attr_bluetooth_enable.attr, - NULL -}; - -static umode_t bluetooth_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return tp_features.bluetooth ? attr->mode : 0; -} - -static const struct attribute_group bluetooth_attr_group = { - .is_visible = bluetooth_attr_is_visible, - .attrs = bluetooth_attributes, -}; - -static const struct tpacpi_rfk_ops bluetooth_tprfk_ops = { - .get_status = bluetooth_get_status, - .set_status = bluetooth_set_status, -}; - -static void bluetooth_shutdown(void) -{ - /* Order firmware to save current state to NVRAM */ - if (!acpi_evalf(NULL, NULL, "\\BLTH", "vd", - TP_ACPI_BLTH_SAVE_STATE)) - pr_notice("failed to save bluetooth state to NVRAM\n"); - else - vdbg_printk(TPACPI_DBG_RFKILL, - "bluetooth state saved to NVRAM\n"); -} - -static void bluetooth_exit(void) -{ - tpacpi_destroy_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID); - bluetooth_shutdown(); -} - -static const struct dmi_system_id fwbug_list[] __initconst = { - { - .ident = "ThinkPad E485", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20KU"), - }, - }, - { - .ident = "ThinkPad E585", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20KV"), - }, - }, - { - .ident = "ThinkPad A285 - 20MW", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20MW"), - }, - }, - { - .ident = "ThinkPad A285 - 20MX", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20MX"), - }, - }, - { - .ident = "ThinkPad A485 - 20MU", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20MU"), - }, - }, - { - .ident = "ThinkPad A485 - 20MV", - .driver_data = &quirk_btusb_bug, - .matches = { - DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), - DMI_MATCH(DMI_BOARD_NAME, "20MV"), - }, - }, - {} -}; - -static const struct pci_device_id fwbug_cards_ids[] __initconst = { - { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24F3) }, - { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x24FD) }, - { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x2526) }, - {} -}; - - -static int __init have_bt_fwbug(void) -{ - /* - * Some AMD based ThinkPads have a firmware bug that calling - * "GBDC" will cause bluetooth on Intel wireless cards blocked - */ - if (tp_features.quirks && tp_features.quirks->btusb_bug && - pci_dev_present(fwbug_cards_ids)) { - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - FW_BUG "disable bluetooth subdriver for Intel cards\n"); - return 1; - } else - return 0; -} - -static int __init bluetooth_init(struct ibm_init_struct *iibm) -{ - int res; - int status = 0; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "initializing bluetooth subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(hkey); - - /* bluetooth not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, - G4x, R30, R31, R40e, R50e, T20-22, X20-21 */ - tp_features.bluetooth = !have_bt_fwbug() && hkey_handle && - acpi_evalf(hkey_handle, &status, "GBDC", "qd"); - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "bluetooth is %s, status 0x%02x\n", - str_supported(tp_features.bluetooth), - status); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_bluetoothemul) { - tp_features.bluetooth = 1; - pr_info("bluetooth switch emulation enabled\n"); - } else -#endif - if (tp_features.bluetooth && - !(status & TP_ACPI_BLUETOOTH_HWPRESENT)) { - /* no bluetooth hardware present in system */ - tp_features.bluetooth = 0; - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "bluetooth hardware not installed\n"); - } - - if (!tp_features.bluetooth) - return -ENODEV; - - res = tpacpi_new_rfkill(TPACPI_RFK_BLUETOOTH_SW_ID, - &bluetooth_tprfk_ops, - RFKILL_TYPE_BLUETOOTH, - TPACPI_RFK_BLUETOOTH_SW_NAME, - true); - return res; -} - -/* procfs -------------------------------------------------------------- */ -static int bluetooth_read(struct seq_file *m) -{ - return tpacpi_rfk_procfs_read(TPACPI_RFK_BLUETOOTH_SW_ID, m); -} - -static int bluetooth_write(char *buf) -{ - return tpacpi_rfk_procfs_write(TPACPI_RFK_BLUETOOTH_SW_ID, buf); -} - -static struct ibm_struct bluetooth_driver_data = { - .name = "bluetooth", - .read = bluetooth_read, - .write = bluetooth_write, - .exit = bluetooth_exit, - .shutdown = bluetooth_shutdown, -}; - -/************************************************************************* - * Wan subdriver - */ - -enum { - /* ACPI GWAN/SWAN bits */ - TP_ACPI_WANCARD_HWPRESENT = 0x01, /* Wan hw available */ - TP_ACPI_WANCARD_RADIOSSW = 0x02, /* Wan radio enabled */ - TP_ACPI_WANCARD_RESUMECTRL = 0x04, /* Wan state at resume: - 0 = disable, 1 = enable */ -}; - -#define TPACPI_RFK_WWAN_SW_NAME "tpacpi_wwan_sw" - -static int wan_get_status(void) -{ - int status; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_wwanemul) - return (tpacpi_wwan_emulstate) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -#endif - - if (!acpi_evalf(hkey_handle, &status, "GWAN", "d")) - return -EIO; - - return ((status & TP_ACPI_WANCARD_RADIOSSW) != 0) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -} - -static int wan_set_status(enum tpacpi_rfkill_state state) -{ - int status; - - vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s wwan\n", - str_enable_disable(state == TPACPI_RFK_RADIO_ON)); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_wwanemul) { - tpacpi_wwan_emulstate = (state == TPACPI_RFK_RADIO_ON); - return 0; - } -#endif - - if (state == TPACPI_RFK_RADIO_ON) - status = TP_ACPI_WANCARD_RADIOSSW - | TP_ACPI_WANCARD_RESUMECTRL; - else - status = 0; - - if (!acpi_evalf(hkey_handle, NULL, "SWAN", "vd", status)) - return -EIO; - - return 0; -} - -/* sysfs wan enable ---------------------------------------------------- */ -static ssize_t wan_enable_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - return tpacpi_rfk_sysfs_enable_show(TPACPI_RFK_WWAN_SW_ID, - attr, buf); -} - -static ssize_t wan_enable_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - return tpacpi_rfk_sysfs_enable_store(TPACPI_RFK_WWAN_SW_ID, - attr, buf, count); -} - -static DEVICE_ATTR(wwan_enable, S_IWUSR | S_IRUGO, - wan_enable_show, wan_enable_store); - -/* --------------------------------------------------------------------- */ - -static struct attribute *wan_attributes[] = { - &dev_attr_wwan_enable.attr, - NULL -}; - -static umode_t wan_attr_is_visible(struct kobject *kobj, struct attribute *attr, - int n) -{ - return tp_features.wan ? attr->mode : 0; -} - -static const struct attribute_group wan_attr_group = { - .is_visible = wan_attr_is_visible, - .attrs = wan_attributes, -}; - -static const struct tpacpi_rfk_ops wan_tprfk_ops = { - .get_status = wan_get_status, - .set_status = wan_set_status, -}; - -static void wan_shutdown(void) -{ - /* Order firmware to save current state to NVRAM */ - if (!acpi_evalf(NULL, NULL, "\\WGSV", "vd", - TP_ACPI_WGSV_SAVE_STATE)) - pr_notice("failed to save WWAN state to NVRAM\n"); - else - vdbg_printk(TPACPI_DBG_RFKILL, - "WWAN state saved to NVRAM\n"); -} - -static void wan_exit(void) -{ - tpacpi_destroy_rfkill(TPACPI_RFK_WWAN_SW_ID); - wan_shutdown(); -} - -static int __init wan_init(struct ibm_init_struct *iibm) -{ - int res; - int status = 0; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "initializing wan subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(hkey); - - tp_features.wan = hkey_handle && - acpi_evalf(hkey_handle, &status, "GWAN", "qd"); - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "wan is %s, status 0x%02x\n", - str_supported(tp_features.wan), - status); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_wwanemul) { - tp_features.wan = 1; - pr_info("wwan switch emulation enabled\n"); - } else -#endif - if (tp_features.wan && - !(status & TP_ACPI_WANCARD_HWPRESENT)) { - /* no wan hardware present in system */ - tp_features.wan = 0; - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "wan hardware not installed\n"); - } - - if (!tp_features.wan) - return -ENODEV; - - res = tpacpi_new_rfkill(TPACPI_RFK_WWAN_SW_ID, - &wan_tprfk_ops, - RFKILL_TYPE_WWAN, - TPACPI_RFK_WWAN_SW_NAME, - true); - return res; -} - -/* procfs -------------------------------------------------------------- */ -static int wan_read(struct seq_file *m) -{ - return tpacpi_rfk_procfs_read(TPACPI_RFK_WWAN_SW_ID, m); -} - -static int wan_write(char *buf) -{ - return tpacpi_rfk_procfs_write(TPACPI_RFK_WWAN_SW_ID, buf); -} - -static struct ibm_struct wan_driver_data = { - .name = "wan", - .read = wan_read, - .write = wan_write, - .exit = wan_exit, - .shutdown = wan_shutdown, -}; - -/************************************************************************* - * UWB subdriver - */ - -enum { - /* ACPI GUWB/SUWB bits */ - TP_ACPI_UWB_HWPRESENT = 0x01, /* UWB hw available */ - TP_ACPI_UWB_RADIOSSW = 0x02, /* UWB radio enabled */ -}; - -#define TPACPI_RFK_UWB_SW_NAME "tpacpi_uwb_sw" - -static int uwb_get_status(void) -{ - int status; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_uwbemul) - return (tpacpi_uwb_emulstate) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -#endif - - if (!acpi_evalf(hkey_handle, &status, "GUWB", "d")) - return -EIO; - - return ((status & TP_ACPI_UWB_RADIOSSW) != 0) ? - TPACPI_RFK_RADIO_ON : TPACPI_RFK_RADIO_OFF; -} - -static int uwb_set_status(enum tpacpi_rfkill_state state) -{ - int status; - - vdbg_printk(TPACPI_DBG_RFKILL, "will attempt to %s UWB\n", - str_enable_disable(state == TPACPI_RFK_RADIO_ON)); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_uwbemul) { - tpacpi_uwb_emulstate = (state == TPACPI_RFK_RADIO_ON); - return 0; - } -#endif - - if (state == TPACPI_RFK_RADIO_ON) - status = TP_ACPI_UWB_RADIOSSW; - else - status = 0; - - if (!acpi_evalf(hkey_handle, NULL, "SUWB", "vd", status)) - return -EIO; - - return 0; -} - -/* --------------------------------------------------------------------- */ - -static const struct tpacpi_rfk_ops uwb_tprfk_ops = { - .get_status = uwb_get_status, - .set_status = uwb_set_status, -}; - -static void uwb_exit(void) -{ - tpacpi_destroy_rfkill(TPACPI_RFK_UWB_SW_ID); -} - -static int __init uwb_init(struct ibm_init_struct *iibm) -{ - int res; - int status = 0; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "initializing uwb subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(hkey); - - tp_features.uwb = hkey_handle && - acpi_evalf(hkey_handle, &status, "GUWB", "qd"); - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_RFKILL, - "uwb is %s, status 0x%02x\n", - str_supported(tp_features.uwb), - status); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - if (dbg_uwbemul) { - tp_features.uwb = 1; - pr_info("uwb switch emulation enabled\n"); - } else -#endif - if (tp_features.uwb && - !(status & TP_ACPI_UWB_HWPRESENT)) { - /* no uwb hardware present in system */ - tp_features.uwb = 0; - dbg_printk(TPACPI_DBG_INIT, - "uwb hardware not installed\n"); - } - - if (!tp_features.uwb) - return -ENODEV; - - res = tpacpi_new_rfkill(TPACPI_RFK_UWB_SW_ID, - &uwb_tprfk_ops, - RFKILL_TYPE_UWB, - TPACPI_RFK_UWB_SW_NAME, - false); - return res; -} - -static struct ibm_struct uwb_driver_data = { - .name = "uwb", - .exit = uwb_exit, - .flags.experimental = 1, -}; - -/************************************************************************* - * Video subdriver - */ - -#ifdef CONFIG_THINKPAD_ACPI_VIDEO - -enum video_access_mode { - TPACPI_VIDEO_NONE = 0, - TPACPI_VIDEO_570, /* 570 */ - TPACPI_VIDEO_770, /* 600e/x, 770e, 770x */ - TPACPI_VIDEO_NEW, /* all others */ -}; - -enum { /* video status flags, based on VIDEO_570 */ - TP_ACPI_VIDEO_S_LCD = 0x01, /* LCD output enabled */ - TP_ACPI_VIDEO_S_CRT = 0x02, /* CRT output enabled */ - TP_ACPI_VIDEO_S_DVI = 0x08, /* DVI output enabled */ -}; - -enum { /* TPACPI_VIDEO_570 constants */ - TP_ACPI_VIDEO_570_PHSCMD = 0x87, /* unknown magic constant :( */ - TP_ACPI_VIDEO_570_PHSMASK = 0x03, /* PHS bits that map to - * video_status_flags */ - TP_ACPI_VIDEO_570_PHS2CMD = 0x8b, /* unknown magic constant :( */ - TP_ACPI_VIDEO_570_PHS2SET = 0x80, /* unknown magic constant :( */ -}; - -static enum video_access_mode video_supported; -static int video_orig_autosw; - -static int video_autosw_get(void); -static int video_autosw_set(int enable); - -TPACPI_HANDLE(vid, root, - "\\_SB.PCI.AGP.VGA", /* 570 */ - "\\_SB.PCI0.AGP0.VID0", /* 600e/x, 770x */ - "\\_SB.PCI0.VID0", /* 770e */ - "\\_SB.PCI0.VID", /* A21e, G4x, R50e, X30, X40 */ - "\\_SB.PCI0.AGP.VGA", /* X100e and a few others */ - "\\_SB.PCI0.AGP.VID", /* all others */ - ); /* R30, R31 */ - -TPACPI_HANDLE(vid2, root, "\\_SB.PCI0.AGPB.VID"); /* G41 */ - -static int __init video_init(struct ibm_init_struct *iibm) -{ - int ivga; - - vdbg_printk(TPACPI_DBG_INIT, "initializing video subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(vid); - if (tpacpi_is_ibm()) - TPACPI_ACPIHANDLE_INIT(vid2); - - if (vid2_handle && acpi_evalf(NULL, &ivga, "\\IVGA", "d") && ivga) - /* G41, assume IVGA doesn't change */ - vid_handle = vid2_handle; - - if (!vid_handle) - /* video switching not supported on R30, R31 */ - video_supported = TPACPI_VIDEO_NONE; - else if (tpacpi_is_ibm() && - acpi_evalf(vid_handle, &video_orig_autosw, "SWIT", "qd")) - /* 570 */ - video_supported = TPACPI_VIDEO_570; - else if (tpacpi_is_ibm() && - acpi_evalf(vid_handle, &video_orig_autosw, "^VADL", "qd")) - /* 600e/x, 770e, 770x */ - video_supported = TPACPI_VIDEO_770; - else - /* all others */ - video_supported = TPACPI_VIDEO_NEW; - - vdbg_printk(TPACPI_DBG_INIT, "video is %s, mode %d\n", - str_supported(video_supported != TPACPI_VIDEO_NONE), - video_supported); - - return (video_supported != TPACPI_VIDEO_NONE) ? 0 : -ENODEV; -} - -static void video_exit(void) -{ - dbg_printk(TPACPI_DBG_EXIT, - "restoring original video autoswitch mode\n"); - if (video_autosw_set(video_orig_autosw)) - pr_err("error while trying to restore original video autoswitch mode\n"); -} - -static int video_outputsw_get(void) -{ - int status = 0; - int i; - - switch (video_supported) { - case TPACPI_VIDEO_570: - if (!acpi_evalf(NULL, &i, "\\_SB.PHS", "dd", - TP_ACPI_VIDEO_570_PHSCMD)) - return -EIO; - status = i & TP_ACPI_VIDEO_570_PHSMASK; - break; - case TPACPI_VIDEO_770: - if (!acpi_evalf(NULL, &i, "\\VCDL", "d")) - return -EIO; - if (i) - status |= TP_ACPI_VIDEO_S_LCD; - if (!acpi_evalf(NULL, &i, "\\VCDC", "d")) - return -EIO; - if (i) - status |= TP_ACPI_VIDEO_S_CRT; - break; - case TPACPI_VIDEO_NEW: - if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 1) || - !acpi_evalf(NULL, &i, "\\VCDC", "d")) - return -EIO; - if (i) - status |= TP_ACPI_VIDEO_S_CRT; - - if (!acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0) || - !acpi_evalf(NULL, &i, "\\VCDL", "d")) - return -EIO; - if (i) - status |= TP_ACPI_VIDEO_S_LCD; - if (!acpi_evalf(NULL, &i, "\\VCDD", "d")) - return -EIO; - if (i) - status |= TP_ACPI_VIDEO_S_DVI; - break; - default: - return -ENOSYS; - } - - return status; -} - -static int video_outputsw_set(int status) -{ - int autosw; - int res = 0; - - switch (video_supported) { - case TPACPI_VIDEO_570: - res = acpi_evalf(NULL, NULL, - "\\_SB.PHS2", "vdd", - TP_ACPI_VIDEO_570_PHS2CMD, - status | TP_ACPI_VIDEO_570_PHS2SET); - break; - case TPACPI_VIDEO_770: - autosw = video_autosw_get(); - if (autosw < 0) - return autosw; - - res = video_autosw_set(1); - if (res) - return res; - res = acpi_evalf(vid_handle, NULL, - "ASWT", "vdd", status * 0x100, 0); - if (!autosw && video_autosw_set(autosw)) { - pr_err("video auto-switch left enabled due to error\n"); - return -EIO; - } - break; - case TPACPI_VIDEO_NEW: - res = acpi_evalf(NULL, NULL, "\\VUPS", "vd", 0x80) && - acpi_evalf(NULL, NULL, "\\VSDS", "vdd", status, 1); - break; - default: - return -ENOSYS; - } - - return (res) ? 0 : -EIO; -} - -static int video_autosw_get(void) -{ - int autosw = 0; - - switch (video_supported) { - case TPACPI_VIDEO_570: - if (!acpi_evalf(vid_handle, &autosw, "SWIT", "d")) - return -EIO; - break; - case TPACPI_VIDEO_770: - case TPACPI_VIDEO_NEW: - if (!acpi_evalf(vid_handle, &autosw, "^VDEE", "d")) - return -EIO; - break; - default: - return -ENOSYS; - } - - return autosw & 1; -} - -static int video_autosw_set(int enable) -{ - if (!acpi_evalf(vid_handle, NULL, "_DOS", "vd", (enable) ? 1 : 0)) - return -EIO; - return 0; -} - -static int video_outputsw_cycle(void) -{ - int autosw = video_autosw_get(); - int res; - - if (autosw < 0) - return autosw; - - switch (video_supported) { - case TPACPI_VIDEO_570: - res = video_autosw_set(1); - if (res) - return res; - res = acpi_evalf(ec_handle, NULL, "_Q16", "v"); - break; - case TPACPI_VIDEO_770: - case TPACPI_VIDEO_NEW: - res = video_autosw_set(1); - if (res) - return res; - res = acpi_evalf(vid_handle, NULL, "VSWT", "v"); - break; - default: - return -ENOSYS; - } - if (!autosw && video_autosw_set(autosw)) { - pr_err("video auto-switch left enabled due to error\n"); - return -EIO; - } - - return (res) ? 0 : -EIO; -} - -static int video_expand_toggle(void) -{ - switch (video_supported) { - case TPACPI_VIDEO_570: - return acpi_evalf(ec_handle, NULL, "_Q17", "v") ? - 0 : -EIO; - case TPACPI_VIDEO_770: - return acpi_evalf(vid_handle, NULL, "VEXP", "v") ? - 0 : -EIO; - case TPACPI_VIDEO_NEW: - return acpi_evalf(NULL, NULL, "\\VEXP", "v") ? - 0 : -EIO; - default: - return -ENOSYS; - } - /* not reached */ -} - -static int video_read(struct seq_file *m) -{ - int status, autosw; - - if (video_supported == TPACPI_VIDEO_NONE) { - seq_printf(m, "status:\t\tnot supported\n"); - return 0; - } - - /* Even reads can crash X.org, so... */ - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - status = video_outputsw_get(); - if (status < 0) - return status; - - autosw = video_autosw_get(); - if (autosw < 0) - return autosw; - - seq_printf(m, "status:\t\tsupported\n"); - seq_printf(m, "lcd:\t\t%s\n", str_enabled_disabled(status & BIT(0))); - seq_printf(m, "crt:\t\t%s\n", str_enabled_disabled(status & BIT(1))); - if (video_supported == TPACPI_VIDEO_NEW) - seq_printf(m, "dvi:\t\t%s\n", str_enabled_disabled(status & BIT(3))); - seq_printf(m, "auto:\t\t%s\n", str_enabled_disabled(autosw & BIT(0))); - seq_printf(m, "commands:\tlcd_enable, lcd_disable\n"); - seq_printf(m, "commands:\tcrt_enable, crt_disable\n"); - if (video_supported == TPACPI_VIDEO_NEW) - seq_printf(m, "commands:\tdvi_enable, dvi_disable\n"); - seq_printf(m, "commands:\tauto_enable, auto_disable\n"); - seq_printf(m, "commands:\tvideo_switch, expand_toggle\n"); - - return 0; -} - -static int video_write(char *buf) -{ - char *cmd; - int enable, disable, status; - int res; - - if (video_supported == TPACPI_VIDEO_NONE) - return -ENODEV; - - /* Even reads can crash X.org, let alone writes... */ - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - enable = 0; - disable = 0; - - while ((cmd = strsep(&buf, ","))) { - if (strstarts(cmd, "lcd_enable")) { - enable |= TP_ACPI_VIDEO_S_LCD; - } else if (strstarts(cmd, "lcd_disable")) { - disable |= TP_ACPI_VIDEO_S_LCD; - } else if (strstarts(cmd, "crt_enable")) { - enable |= TP_ACPI_VIDEO_S_CRT; - } else if (strstarts(cmd, "crt_disable")) { - disable |= TP_ACPI_VIDEO_S_CRT; - } else if (video_supported == TPACPI_VIDEO_NEW && - strstarts(cmd, "dvi_enable")) { - enable |= TP_ACPI_VIDEO_S_DVI; - } else if (video_supported == TPACPI_VIDEO_NEW && - strstarts(cmd, "dvi_disable")) { - disable |= TP_ACPI_VIDEO_S_DVI; - } else if (strstarts(cmd, "auto_enable")) { - res = video_autosw_set(1); - if (res) - return res; - } else if (strstarts(cmd, "auto_disable")) { - res = video_autosw_set(0); - if (res) - return res; - } else if (strstarts(cmd, "video_switch")) { - res = video_outputsw_cycle(); - if (res) - return res; - } else if (strstarts(cmd, "expand_toggle")) { - res = video_expand_toggle(); - if (res) - return res; - } else - return -EINVAL; - } - - if (enable || disable) { - status = video_outputsw_get(); - if (status < 0) - return status; - res = video_outputsw_set((status & ~disable) | enable); - if (res) - return res; - } - - return 0; -} - -static struct ibm_struct video_driver_data = { - .name = "video", - .read = video_read, - .write = video_write, - .exit = video_exit, -}; - -#endif /* CONFIG_THINKPAD_ACPI_VIDEO */ - -/************************************************************************* - * Keyboard backlight subdriver - */ - -static enum led_brightness kbdlight_brightness; -static DEFINE_MUTEX(kbdlight_mutex); - -static int kbdlight_set_level(int level) -{ - int ret = 0; - - if (!hkey_handle) - return -ENXIO; - - mutex_lock(&kbdlight_mutex); - - if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level)) - ret = -EIO; - else - kbdlight_brightness = level; - - mutex_unlock(&kbdlight_mutex); - - return ret; -} - -static int kbdlight_get_level(void) -{ - int status = 0; - - if (!hkey_handle) - return -ENXIO; - - if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0)) - return -EIO; - - if (status < 0) - return status; - - return status & 0x3; -} - -static bool kbdlight_is_supported(void) -{ - int status = 0; - - if (!hkey_handle) - return false; - - if (!acpi_has_method(hkey_handle, "MLCG")) { - vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n"); - return false; - } - - if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) { - vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n"); - return false; - } - - if (status < 0) { - vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status); - return false; - } - - vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status); - /* - * Guessed test for keyboard backlight: - * - * Machines with backlight keyboard return: - * b010100000010000000XX - ThinkPad X1 Carbon 3rd - * b110100010010000000XX - ThinkPad x230 - * b010100000010000000XX - ThinkPad x240 - * b010100000010000000XX - ThinkPad W541 - * (XX is current backlight level) - * - * Machines without backlight keyboard return: - * b10100001000000000000 - ThinkPad x230 - * b10110001000000000000 - ThinkPad E430 - * b00000000000000000000 - ThinkPad E450 - * - * Candidate BITs for detection test (XOR): - * b01000000001000000000 - * ^ - */ - return status & BIT(9); -} - -static int kbdlight_sysfs_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - return kbdlight_set_level(brightness); -} - -static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev) -{ - int level; - - level = kbdlight_get_level(); - if (level < 0) - return 0; - - return level; -} - -static struct tpacpi_led_classdev tpacpi_led_kbdlight = { - .led_classdev = { - .name = "tpacpi::kbd_backlight", - .max_brightness = 2, - .flags = LED_BRIGHT_HW_CHANGED, - .brightness_set_blocking = &kbdlight_sysfs_set, - .brightness_get = &kbdlight_sysfs_get, - } -}; - -static int __init kbdlight_init(struct ibm_init_struct *iibm) -{ - int rc; - - vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(hkey); - - if (!kbdlight_is_supported()) { - tp_features.kbdlight = 0; - vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n"); - return -ENODEV; - } - - kbdlight_brightness = kbdlight_sysfs_get(NULL); - tp_features.kbdlight = 1; - - rc = led_classdev_register(&tpacpi_pdev->dev, - &tpacpi_led_kbdlight.led_classdev); - if (rc < 0) { - tp_features.kbdlight = 0; - return rc; - } - - tpacpi_hotkey_driver_mask_set(hotkey_driver_mask | - TP_ACPI_HKEY_KBD_LIGHT_MASK); - return 0; -} - -static void kbdlight_exit(void) -{ - led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev); -} - -static int kbdlight_set_level_and_update(int level) -{ - int ret; - struct led_classdev *led_cdev; - - ret = kbdlight_set_level(level); - led_cdev = &tpacpi_led_kbdlight.led_classdev; - - if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED)) - led_cdev->brightness = level; - - return ret; -} - -static int kbdlight_read(struct seq_file *m) -{ - int level; - - if (!tp_features.kbdlight) { - seq_printf(m, "status:\t\tnot supported\n"); - } else { - level = kbdlight_get_level(); - if (level < 0) - seq_printf(m, "status:\t\terror %d\n", level); - else - seq_printf(m, "status:\t\t%d\n", level); - seq_printf(m, "commands:\t0, 1, 2\n"); - } - - return 0; -} - -static int kbdlight_write(char *buf) -{ - char *cmd; - int res, level = -EINVAL; - - if (!tp_features.kbdlight) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - res = kstrtoint(cmd, 10, &level); - if (res < 0) - return res; - } - - if (level >= 3 || level < 0) - return -EINVAL; - - return kbdlight_set_level_and_update(level); -} - -static void kbdlight_suspend(void) -{ - struct led_classdev *led_cdev; - - if (!tp_features.kbdlight) - return; - - led_cdev = &tpacpi_led_kbdlight.led_classdev; - led_update_brightness(led_cdev); - led_classdev_suspend(led_cdev); -} - -static void kbdlight_resume(void) -{ - if (!tp_features.kbdlight) - return; - - led_classdev_resume(&tpacpi_led_kbdlight.led_classdev); -} - -static struct ibm_struct kbdlight_driver_data = { - .name = "kbdlight", - .read = kbdlight_read, - .write = kbdlight_write, - .suspend = kbdlight_suspend, - .resume = kbdlight_resume, - .exit = kbdlight_exit, -}; - -/************************************************************************* - * Light (thinklight) subdriver - */ - -TPACPI_HANDLE(lght, root, "\\LGHT"); /* A21e, A2xm/p, T20-22, X20-21 */ -TPACPI_HANDLE(ledb, ec, "LEDB"); /* G4x */ - -static int light_get_status(void) -{ - int status = 0; - - if (tp_features.light_status) { - if (!acpi_evalf(ec_handle, &status, "KBLT", "d")) - return -EIO; - return (!!status); - } - - return -ENXIO; -} - -static int light_set_status(int status) -{ - int rc; - - if (tp_features.light) { - if (cmos_handle) { - rc = acpi_evalf(cmos_handle, NULL, NULL, "vd", - (status) ? - TP_CMOS_THINKLIGHT_ON : - TP_CMOS_THINKLIGHT_OFF); - } else { - rc = acpi_evalf(lght_handle, NULL, NULL, "vd", - (status) ? 1 : 0); - } - return (rc) ? 0 : -EIO; - } - - return -ENXIO; -} - -static int light_sysfs_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - return light_set_status((brightness != LED_OFF) ? - TPACPI_LED_ON : TPACPI_LED_OFF); -} - -static enum led_brightness light_sysfs_get(struct led_classdev *led_cdev) -{ - return (light_get_status() == 1) ? LED_ON : LED_OFF; -} - -static struct tpacpi_led_classdev tpacpi_led_thinklight = { - .led_classdev = { - .name = "tpacpi::thinklight", - .max_brightness = 1, - .brightness_set_blocking = &light_sysfs_set, - .brightness_get = &light_sysfs_get, - } -}; - -static int __init light_init(struct ibm_init_struct *iibm) -{ - int rc; - - vdbg_printk(TPACPI_DBG_INIT, "initializing light subdriver\n"); - - if (tpacpi_is_ibm()) { - TPACPI_ACPIHANDLE_INIT(ledb); - TPACPI_ACPIHANDLE_INIT(lght); - } - TPACPI_ACPIHANDLE_INIT(cmos); - - /* light not supported on 570, 600e/x, 770e, 770x, G4x, R30, R31 */ - tp_features.light = (cmos_handle || lght_handle) && !ledb_handle; - - if (tp_features.light) - /* light status not supported on - 570, 600e/x, 770e, 770x, G4x, R30, R31, R32, X20 */ - tp_features.light_status = - acpi_evalf(ec_handle, NULL, "KBLT", "qv"); - - vdbg_printk(TPACPI_DBG_INIT, "light is %s, light status is %s\n", - str_supported(tp_features.light), - str_supported(tp_features.light_status)); - - if (!tp_features.light) - return -ENODEV; - - rc = led_classdev_register(&tpacpi_pdev->dev, - &tpacpi_led_thinklight.led_classdev); - - if (rc < 0) { - tp_features.light = 0; - tp_features.light_status = 0; - } else { - rc = 0; - } - - return rc; -} - -static void light_exit(void) -{ - led_classdev_unregister(&tpacpi_led_thinklight.led_classdev); -} - -static int light_read(struct seq_file *m) -{ - int status; - - if (!tp_features.light) { - seq_printf(m, "status:\t\tnot supported\n"); - } else if (!tp_features.light_status) { - seq_printf(m, "status:\t\tunknown\n"); - seq_printf(m, "commands:\ton, off\n"); - } else { - status = light_get_status(); - if (status < 0) - return status; - seq_printf(m, "status:\t\t%s\n", str_on_off(status & BIT(0))); - seq_printf(m, "commands:\ton, off\n"); - } - - return 0; -} - -static int light_write(char *buf) -{ - char *cmd; - int newstatus = 0; - - if (!tp_features.light) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - if (strstarts(cmd, "on")) { - newstatus = 1; - } else if (strstarts(cmd, "off")) { - newstatus = 0; - } else - return -EINVAL; - } - - return light_set_status(newstatus); -} - -static struct ibm_struct light_driver_data = { - .name = "light", - .read = light_read, - .write = light_write, - .exit = light_exit, -}; - -/************************************************************************* - * CMOS subdriver - */ - -/* sysfs cmos_command -------------------------------------------------- */ -static ssize_t cmos_command_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long cmos_cmd; - int res; - - if (parse_strtoul(buf, 21, &cmos_cmd)) - return -EINVAL; - - res = issue_thinkpad_cmos_command(cmos_cmd); - return (res) ? res : count; -} - -static DEVICE_ATTR_WO(cmos_command); - -static struct attribute *cmos_attributes[] = { - &dev_attr_cmos_command.attr, - NULL -}; - -static umode_t cmos_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return cmos_handle ? attr->mode : 0; -} - -static const struct attribute_group cmos_attr_group = { - .is_visible = cmos_attr_is_visible, - .attrs = cmos_attributes, -}; - -/* --------------------------------------------------------------------- */ - -static int __init cmos_init(struct ibm_init_struct *iibm) -{ - vdbg_printk(TPACPI_DBG_INIT, - "initializing cmos commands subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(cmos); - - vdbg_printk(TPACPI_DBG_INIT, "cmos commands are %s\n", - str_supported(cmos_handle != NULL)); - - return cmos_handle ? 0 : -ENODEV; -} - -static int cmos_read(struct seq_file *m) -{ - /* cmos not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p, - R30, R31, T20-22, X20-21 */ - if (!cmos_handle) - seq_printf(m, "status:\t\tnot supported\n"); - else { - seq_printf(m, "status:\t\tsupported\n"); - seq_printf(m, "commands:\t ( is 0-21)\n"); - } - - return 0; -} - -static int cmos_write(char *buf) -{ - char *cmd; - int cmos_cmd, res; - - while ((cmd = strsep(&buf, ","))) { - if (sscanf(cmd, "%u", &cmos_cmd) == 1 && - cmos_cmd >= 0 && cmos_cmd <= 21) { - /* cmos_cmd set */ - } else - return -EINVAL; - - res = issue_thinkpad_cmos_command(cmos_cmd); - if (res) - return res; - } - - return 0; -} - -static struct ibm_struct cmos_driver_data = { - .name = "cmos", - .read = cmos_read, - .write = cmos_write, -}; - -/************************************************************************* - * LED subdriver - */ - -enum led_access_mode { - TPACPI_LED_NONE = 0, - TPACPI_LED_570, /* 570 */ - TPACPI_LED_OLD, /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ - TPACPI_LED_NEW, /* all others */ -}; - -enum { /* For TPACPI_LED_OLD */ - TPACPI_LED_EC_HLCL = 0x0c, /* EC reg to get led to power on */ - TPACPI_LED_EC_HLBL = 0x0d, /* EC reg to blink a lit led */ - TPACPI_LED_EC_HLMS = 0x0e, /* EC reg to select led to command */ -}; - -static enum led_access_mode led_supported; - -static acpi_handle led_handle; - -#define TPACPI_LED_NUMLEDS 16 -static struct tpacpi_led_classdev *tpacpi_leds; -static enum led_status_t tpacpi_led_state_cache[TPACPI_LED_NUMLEDS]; -static const char * const tpacpi_led_names[TPACPI_LED_NUMLEDS] = { - /* there's a limit of 19 chars + NULL before 2.6.26 */ - "tpacpi::power", - "tpacpi:orange:batt", - "tpacpi:green:batt", - "tpacpi::dock_active", - "tpacpi::bay_active", - "tpacpi::dock_batt", - "tpacpi::unknown_led", - "tpacpi::standby", - "tpacpi::dock_status1", - "tpacpi::dock_status2", - "tpacpi::lid_logo_dot", - "tpacpi::unknown_led3", - "tpacpi::thinkvantage", -}; -#define TPACPI_SAFE_LEDS 0x1481U - -static inline bool tpacpi_is_led_restricted(const unsigned int led) -{ -#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS - return false; -#else - return (1U & (TPACPI_SAFE_LEDS >> led)) == 0; -#endif -} - -static int led_get_status(const unsigned int led) -{ - int status; - enum led_status_t led_s; - - switch (led_supported) { - case TPACPI_LED_570: - if (!acpi_evalf(ec_handle, - &status, "GLED", "dd", 1 << led)) - return -EIO; - led_s = (status == 0) ? - TPACPI_LED_OFF : - ((status == 1) ? - TPACPI_LED_ON : - TPACPI_LED_BLINK); - tpacpi_led_state_cache[led] = led_s; - return led_s; - default: - return -ENXIO; - } - - /* not reached */ -} - -static int led_set_status(const unsigned int led, - const enum led_status_t ledstatus) -{ - /* off, on, blink. Index is led_status_t */ - static const unsigned int led_sled_arg1[] = { 0, 1, 3 }; - static const unsigned int led_led_arg1[] = { 0, 0x80, 0xc0 }; - - int rc = 0; - - switch (led_supported) { - case TPACPI_LED_570: - /* 570 */ - if (unlikely(led > 7)) - return -EINVAL; - if (unlikely(tpacpi_is_led_restricted(led))) - return -EPERM; - if (!acpi_evalf(led_handle, NULL, NULL, "vdd", - (1 << led), led_sled_arg1[ledstatus])) - return -EIO; - break; - case TPACPI_LED_OLD: - /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20 */ - if (unlikely(led > 7)) - return -EINVAL; - if (unlikely(tpacpi_is_led_restricted(led))) - return -EPERM; - rc = ec_write(TPACPI_LED_EC_HLMS, (1 << led)); - if (rc >= 0) - rc = ec_write(TPACPI_LED_EC_HLBL, - (ledstatus == TPACPI_LED_BLINK) << led); - if (rc >= 0) - rc = ec_write(TPACPI_LED_EC_HLCL, - (ledstatus != TPACPI_LED_OFF) << led); - break; - case TPACPI_LED_NEW: - /* all others */ - if (unlikely(led >= TPACPI_LED_NUMLEDS)) - return -EINVAL; - if (unlikely(tpacpi_is_led_restricted(led))) - return -EPERM; - if (!acpi_evalf(led_handle, NULL, NULL, "vdd", - led, led_led_arg1[ledstatus])) - return -EIO; - break; - default: - return -ENXIO; - } - - if (!rc) - tpacpi_led_state_cache[led] = ledstatus; - - return rc; -} - -static int led_sysfs_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - struct tpacpi_led_classdev *data = container_of(led_cdev, - struct tpacpi_led_classdev, led_classdev); - enum led_status_t new_state; - - if (brightness == LED_OFF) - new_state = TPACPI_LED_OFF; - else if (tpacpi_led_state_cache[data->led] != TPACPI_LED_BLINK) - new_state = TPACPI_LED_ON; - else - new_state = TPACPI_LED_BLINK; - - return led_set_status(data->led, new_state); -} - -static int led_sysfs_blink_set(struct led_classdev *led_cdev, - unsigned long *delay_on, unsigned long *delay_off) -{ - struct tpacpi_led_classdev *data = container_of(led_cdev, - struct tpacpi_led_classdev, led_classdev); - - /* Can we choose the flash rate? */ - if (*delay_on == 0 && *delay_off == 0) { - /* yes. set them to the hardware blink rate (1 Hz) */ - *delay_on = 500; /* ms */ - *delay_off = 500; /* ms */ - } else if ((*delay_on != 500) || (*delay_off != 500)) - return -EINVAL; - - return led_set_status(data->led, TPACPI_LED_BLINK); -} - -static enum led_brightness led_sysfs_get(struct led_classdev *led_cdev) -{ - int rc; - - struct tpacpi_led_classdev *data = container_of(led_cdev, - struct tpacpi_led_classdev, led_classdev); - - rc = led_get_status(data->led); - - if (rc == TPACPI_LED_OFF || rc < 0) - rc = LED_OFF; /* no error handling in led class :( */ - else - rc = LED_FULL; - - return rc; -} - -static void led_exit(void) -{ - unsigned int i; - - for (i = 0; i < TPACPI_LED_NUMLEDS; i++) - led_classdev_unregister(&tpacpi_leds[i].led_classdev); - - kfree(tpacpi_leds); -} - -static int __init tpacpi_init_led(unsigned int led) -{ - /* LEDs with no name don't get registered */ - if (!tpacpi_led_names[led]) - return 0; - - tpacpi_leds[led].led_classdev.brightness_set_blocking = &led_sysfs_set; - tpacpi_leds[led].led_classdev.blink_set = &led_sysfs_blink_set; - if (led_supported == TPACPI_LED_570) - tpacpi_leds[led].led_classdev.brightness_get = &led_sysfs_get; - - tpacpi_leds[led].led_classdev.name = tpacpi_led_names[led]; - tpacpi_leds[led].led_classdev.flags = LED_RETAIN_AT_SHUTDOWN; - tpacpi_leds[led].led = led; - - return led_classdev_register(&tpacpi_pdev->dev, &tpacpi_leds[led].led_classdev); -} - -static const struct tpacpi_quirk led_useful_qtable[] __initconst = { - TPACPI_Q_IBM('1', 'E', 0x009f), /* A30 */ - TPACPI_Q_IBM('1', 'N', 0x009f), /* A31 */ - TPACPI_Q_IBM('1', 'G', 0x009f), /* A31 */ - - TPACPI_Q_IBM('1', 'I', 0x0097), /* T30 */ - TPACPI_Q_IBM('1', 'R', 0x0097), /* T40, T41, T42, R50, R51 */ - TPACPI_Q_IBM('7', '0', 0x0097), /* T43, R52 */ - TPACPI_Q_IBM('1', 'Y', 0x0097), /* T43 */ - TPACPI_Q_IBM('1', 'W', 0x0097), /* R50e */ - TPACPI_Q_IBM('1', 'V', 0x0097), /* R51 */ - TPACPI_Q_IBM('7', '8', 0x0097), /* R51e */ - TPACPI_Q_IBM('7', '6', 0x0097), /* R52 */ - - TPACPI_Q_IBM('1', 'K', 0x00bf), /* X30 */ - TPACPI_Q_IBM('1', 'Q', 0x00bf), /* X31, X32 */ - TPACPI_Q_IBM('1', 'U', 0x00bf), /* X40 */ - TPACPI_Q_IBM('7', '4', 0x00bf), /* X41 */ - TPACPI_Q_IBM('7', '5', 0x00bf), /* X41t */ - - TPACPI_Q_IBM('7', '9', 0x1f97), /* T60 (1) */ - TPACPI_Q_IBM('7', '7', 0x1f97), /* Z60* (1) */ - TPACPI_Q_IBM('7', 'F', 0x1f97), /* Z61* (1) */ - TPACPI_Q_IBM('7', 'B', 0x1fb7), /* X60 (1) */ - - /* (1) - may have excess leds enabled on MSB */ - - /* Defaults (order matters, keep last, don't reorder!) */ - { /* Lenovo */ - .vendor = PCI_VENDOR_ID_LENOVO, - .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, - .quirks = 0x1fffU, - }, - { /* IBM ThinkPads with no EC version string */ - .vendor = PCI_VENDOR_ID_IBM, - .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_UNKNOWN, - .quirks = 0x00ffU, - }, - { /* IBM ThinkPads with EC version string */ - .vendor = PCI_VENDOR_ID_IBM, - .bios = TPACPI_MATCH_ANY, .ec = TPACPI_MATCH_ANY, - .quirks = 0x00bfU, - }, -}; - -static enum led_access_mode __init led_init_detect_mode(void) -{ - acpi_status status; - - if (tpacpi_is_ibm()) { - /* 570 */ - status = acpi_get_handle(ec_handle, "SLED", &led_handle); - if (ACPI_SUCCESS(status)) - return TPACPI_LED_570; - - /* 600e/x, 770e, 770x, A21e, A2xm/p, T20-22, X20-21 */ - status = acpi_get_handle(ec_handle, "SYSL", &led_handle); - if (ACPI_SUCCESS(status)) - return TPACPI_LED_OLD; - } - - /* most others */ - status = acpi_get_handle(ec_handle, "LED", &led_handle); - if (ACPI_SUCCESS(status)) - return TPACPI_LED_NEW; - - /* R30, R31, and unknown firmwares */ - led_handle = NULL; - return TPACPI_LED_NONE; -} - -static int __init led_init(struct ibm_init_struct *iibm) -{ - unsigned int i; - int rc; - unsigned long useful_leds; - - vdbg_printk(TPACPI_DBG_INIT, "initializing LED subdriver\n"); - - led_supported = led_init_detect_mode(); - - if (led_supported != TPACPI_LED_NONE) { - useful_leds = tpacpi_check_quirks(led_useful_qtable, - ARRAY_SIZE(led_useful_qtable)); - - if (!useful_leds) { - led_handle = NULL; - led_supported = TPACPI_LED_NONE; - } - } - - vdbg_printk(TPACPI_DBG_INIT, "LED commands are %s, mode %d\n", - str_supported(led_supported), led_supported); - - if (led_supported == TPACPI_LED_NONE) - return -ENODEV; - - tpacpi_leds = kcalloc(TPACPI_LED_NUMLEDS, sizeof(*tpacpi_leds), - GFP_KERNEL); - if (!tpacpi_leds) { - pr_err("Out of memory for LED data\n"); - return -ENOMEM; - } - - for (i = 0; i < TPACPI_LED_NUMLEDS; i++) { - tpacpi_leds[i].led = -1; - - if (!tpacpi_is_led_restricted(i) && test_bit(i, &useful_leds)) { - rc = tpacpi_init_led(i); - if (rc < 0) { - led_exit(); - return rc; - } - } - } - -#ifdef CONFIG_THINKPAD_ACPI_UNSAFE_LEDS - pr_notice("warning: userspace override of important firmware LEDs is enabled\n"); -#endif - return 0; -} - -#define str_led_status(s) ((s) >= TPACPI_LED_BLINK ? "blinking" : str_on_off(s)) - -static int led_read(struct seq_file *m) -{ - if (!led_supported) { - seq_printf(m, "status:\t\tnot supported\n"); - return 0; - } - seq_printf(m, "status:\t\tsupported\n"); - - if (led_supported == TPACPI_LED_570) { - /* 570 */ - int i, status; - for (i = 0; i < 8; i++) { - status = led_get_status(i); - if (status < 0) - return -EIO; - seq_printf(m, "%d:\t\t%s\n", i, str_led_status(status)); - } - } - - seq_printf(m, "commands:\t on, off, blink ( is 0-15)\n"); - - return 0; -} - -static int led_write(char *buf) -{ - char *cmd; - int led, rc; - enum led_status_t s; - - if (!led_supported) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - if (sscanf(cmd, "%d", &led) != 1) - return -EINVAL; - - if (led < 0 || led > (TPACPI_LED_NUMLEDS - 1)) - return -ENODEV; - - if (tpacpi_leds[led].led < 0) - return -ENODEV; - - if (strstr(cmd, "off")) { - s = TPACPI_LED_OFF; - } else if (strstr(cmd, "on")) { - s = TPACPI_LED_ON; - } else if (strstr(cmd, "blink")) { - s = TPACPI_LED_BLINK; - } else { - return -EINVAL; - } - - rc = led_set_status(led, s); - if (rc < 0) - return rc; - } - - return 0; -} - -static struct ibm_struct led_driver_data = { - .name = "led", - .read = led_read, - .write = led_write, - .exit = led_exit, -}; - -/************************************************************************* - * Beep subdriver - */ - -TPACPI_HANDLE(beep, ec, "BEEP"); /* all except R30, R31 */ - -#define TPACPI_BEEP_Q1 0x0001 - -static const struct tpacpi_quirk beep_quirk_table[] __initconst = { - TPACPI_Q_IBM('I', 'M', TPACPI_BEEP_Q1), /* 570 */ - TPACPI_Q_IBM('I', 'U', TPACPI_BEEP_Q1), /* 570E - unverified */ -}; - -static int __init beep_init(struct ibm_init_struct *iibm) -{ - unsigned long quirks; - - vdbg_printk(TPACPI_DBG_INIT, "initializing beep subdriver\n"); - - TPACPI_ACPIHANDLE_INIT(beep); - - vdbg_printk(TPACPI_DBG_INIT, "beep is %s\n", - str_supported(beep_handle != NULL)); - - quirks = tpacpi_check_quirks(beep_quirk_table, - ARRAY_SIZE(beep_quirk_table)); - - tp_features.beep_needs_two_args = !!(quirks & TPACPI_BEEP_Q1); - - return (beep_handle) ? 0 : -ENODEV; -} - -static int beep_read(struct seq_file *m) -{ - if (!beep_handle) - seq_printf(m, "status:\t\tnot supported\n"); - else { - seq_printf(m, "status:\t\tsupported\n"); - seq_printf(m, "commands:\t ( is 0-17)\n"); - } - - return 0; -} - -static int beep_write(char *buf) -{ - char *cmd; - int beep_cmd; - - if (!beep_handle) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - if (sscanf(cmd, "%u", &beep_cmd) == 1 && - beep_cmd >= 0 && beep_cmd <= 17) { - /* beep_cmd set */ - } else - return -EINVAL; - if (tp_features.beep_needs_two_args) { - if (!acpi_evalf(beep_handle, NULL, NULL, "vdd", - beep_cmd, 0)) - return -EIO; - } else { - if (!acpi_evalf(beep_handle, NULL, NULL, "vd", - beep_cmd)) - return -EIO; - } - } - - return 0; -} - -static struct ibm_struct beep_driver_data = { - .name = "beep", - .read = beep_read, - .write = beep_write, -}; - -/************************************************************************* - * Thermal subdriver - */ - -enum thermal_access_mode { - TPACPI_THERMAL_NONE = 0, /* No thermal support */ - TPACPI_THERMAL_ACPI_TMP07, /* Use ACPI TMP0-7 */ - TPACPI_THERMAL_ACPI_UPDT, /* Use ACPI TMP0-7 with UPDT */ - TPACPI_THERMAL_TPEC_8, /* Use ACPI EC regs, 8 sensors */ - TPACPI_THERMAL_TPEC_12, /* Use ACPI EC regs, 12 sensors */ - TPACPI_THERMAL_TPEC_16, /* Use ACPI EC regs, 16 sensors */ -}; - -enum { /* TPACPI_THERMAL_TPEC_* */ - TP_EC_THERMAL_TMP0 = 0x78, /* ACPI EC regs TMP 0..7 */ - TP_EC_THERMAL_TMP8 = 0xC0, /* ACPI EC regs TMP 8..15 */ - TP_EC_THERMAL_TMP0_NS = 0xA8, /* ACPI EC Non-Standard regs TMP 0..7 */ - TP_EC_THERMAL_TMP8_NS = 0xB8, /* ACPI EC Non-standard regs TMP 8..11 */ - TP_EC_FUNCREV = 0xEF, /* ACPI EC Functional revision */ - TP_EC_THERMAL_TMP_NA = -128, /* ACPI EC sensor not available */ - - TPACPI_THERMAL_SENSOR_NA = -128000, /* Sensor not available */ -}; - - -#define TPACPI_MAX_THERMAL_SENSORS 16 /* Max thermal sensors supported */ -struct ibm_thermal_sensors_struct { - s32 temp[TPACPI_MAX_THERMAL_SENSORS]; -}; - -static const struct tpacpi_quirk thermal_quirk_table[] __initconst = { - /* Non-standard address for thermal registers on some ThinkPads */ - TPACPI_Q_LNV3('R', '1', 'F', true), /* L13 Yoga Gen 2 */ - TPACPI_Q_LNV3('N', '2', 'U', true), /* X13 Yoga Gen 2*/ - TPACPI_Q_LNV3('R', '0', 'R', true), /* L380 */ - TPACPI_Q_LNV3('R', '1', '5', true), /* L13 Yoga Gen 1*/ - TPACPI_Q_LNV3('R', '1', '0', true), /* L390 */ - TPACPI_Q_LNV3('N', '2', 'L', true), /* X13 Yoga Gen 1*/ - TPACPI_Q_LNV3('R', '0', 'T', true), /* 11e Gen5 GL*/ - TPACPI_Q_LNV3('R', '1', 'D', true), /* 11e Gen5 GL-R*/ - TPACPI_Q_LNV3('R', '0', 'V', true), /* 11e Gen5 KL-Y*/ -}; - -static enum thermal_access_mode thermal_read_mode; -static bool thermal_use_labels; -static bool thermal_with_ns_address; /* Non-standard thermal reg address */ - -/* Function to check thermal read mode */ -static enum thermal_access_mode __init thermal_read_mode_check(void) -{ - u8 t, ta1, ta2, ver = 0; - int i; - int acpi_tmp7; - - acpi_tmp7 = acpi_evalf(ec_handle, NULL, "TMP7", "qv"); - - if (thinkpad_id.ec_model) { - /* - * Direct EC access mode: sensors at registers 0x78-0x7F, - * 0xC0-0xC7. Registers return 0x00 for non-implemented, - * thermal sensors return 0x80 when not available. - * - * In some special cases (when Power Supply ID is 0xC2) - * above rule causes thermal control issues. Offset 0xEF - * determines EC version. 0xC0-0xC7 are not thermal registers - * in Ver 3. - */ - if (!acpi_ec_read(TP_EC_FUNCREV, &ver)) - pr_warn("Thinkpad ACPI EC unable to access EC version\n"); - - /* Quirks to check non-standard EC */ - thermal_with_ns_address = tpacpi_check_quirks(thermal_quirk_table, - ARRAY_SIZE(thermal_quirk_table)); - - /* Support for Thinkpads with non-standard address */ - if (thermal_with_ns_address) { - pr_info("ECFW with non-standard thermal registers found\n"); - return TPACPI_THERMAL_TPEC_12; - } - - ta1 = ta2 = 0; - for (i = 0; i < 8; i++) { - if (acpi_ec_read(TP_EC_THERMAL_TMP0 + i, &t)) { - ta1 |= t; - } else { - ta1 = 0; - break; - } - if (ver < 3) { - if (acpi_ec_read(TP_EC_THERMAL_TMP8 + i, &t)) { - ta2 |= t; - } else { - ta1 = 0; - break; - } - } - } - - if (ta1 == 0) { - /* This is sheer paranoia, but we handle it anyway */ - if (acpi_tmp7) { - pr_err("ThinkPad ACPI EC access misbehaving, falling back to ACPI TMPx access mode\n"); - return TPACPI_THERMAL_ACPI_TMP07; - } - pr_err("ThinkPad ACPI EC access misbehaving, disabling thermal sensors access\n"); - return TPACPI_THERMAL_NONE; - } - - if (ver >= 3) { - thermal_use_labels = true; - return TPACPI_THERMAL_TPEC_8; - } - - return (ta2 != 0) ? TPACPI_THERMAL_TPEC_16 : TPACPI_THERMAL_TPEC_8; - } - - if (acpi_tmp7) { - if (tpacpi_is_ibm() && acpi_evalf(ec_handle, NULL, "UPDT", "qv")) { - /* 600e/x, 770e, 770x */ - return TPACPI_THERMAL_ACPI_UPDT; - } - /* IBM/LENOVO DSDT EC.TMPx access, max 8 sensors */ - return TPACPI_THERMAL_ACPI_TMP07; - } - - /* temperatures not supported on 570, G4x, R30, R31, R32 */ - return TPACPI_THERMAL_NONE; -} - -/* idx is zero-based */ -static int thermal_get_sensor(int idx, s32 *value) -{ - int t; - s8 tmp; - char tmpi[5]; - - t = TP_EC_THERMAL_TMP0; - - switch (thermal_read_mode) { -#if TPACPI_MAX_THERMAL_SENSORS >= 16 - case TPACPI_THERMAL_TPEC_16: - if (idx >= 8 && idx <= 15) { - t = TP_EC_THERMAL_TMP8; - idx -= 8; - } -#endif - fallthrough; - case TPACPI_THERMAL_TPEC_8: - if (idx <= 7) { - if (!acpi_ec_read(t + idx, &tmp)) - return -EIO; - *value = tmp * 1000; - return 0; - } - break; - - /* The Non-standard EC uses 12 Thermal areas */ - case TPACPI_THERMAL_TPEC_12: - if (idx >= 12) - return -EINVAL; - - t = idx < 8 ? TP_EC_THERMAL_TMP0_NS + idx : - TP_EC_THERMAL_TMP8_NS + (idx - 8); - - if (!acpi_ec_read(t, &tmp)) - return -EIO; - - *value = tmp * MILLIDEGREE_PER_DEGREE; - return 0; - - case TPACPI_THERMAL_ACPI_UPDT: - if (idx <= 7) { - snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); - if (!acpi_evalf(ec_handle, NULL, "UPDT", "v")) - return -EIO; - if (!acpi_evalf(ec_handle, &t, tmpi, "d")) - return -EIO; - *value = (t - 2732) * 100; - return 0; - } - break; - - case TPACPI_THERMAL_ACPI_TMP07: - if (idx <= 7) { - snprintf(tmpi, sizeof(tmpi), "TMP%c", '0' + idx); - if (!acpi_evalf(ec_handle, &t, tmpi, "d")) - return -EIO; - if (t > 127 || t < -127) - t = TP_EC_THERMAL_TMP_NA; - *value = t * 1000; - return 0; - } - break; - - case TPACPI_THERMAL_NONE: - default: - return -ENOSYS; - } - - return -EINVAL; -} - -static int thermal_get_sensors(struct ibm_thermal_sensors_struct *s) -{ - int res, i, n; - - if (!s) - return -EINVAL; - - if (thermal_read_mode == TPACPI_THERMAL_TPEC_16) - n = 16; - else if (thermal_read_mode == TPACPI_THERMAL_TPEC_12) - n = 12; - else - n = 8; - - for (i = 0 ; i < n; i++) { - res = thermal_get_sensor(i, &s->temp[i]); - if (res) - return res; - } - - return n; -} - -static void thermal_dump_all_sensors(void) -{ - int n, i; - struct ibm_thermal_sensors_struct t; - - n = thermal_get_sensors(&t); - if (n <= 0) - return; - - pr_notice("temperatures (Celsius):"); - - for (i = 0; i < n; i++) { - if (t.temp[i] != TPACPI_THERMAL_SENSOR_NA) - pr_cont(" %d", (int)(t.temp[i] / 1000)); - else - pr_cont(" N/A"); - } - - pr_cont("\n"); -} - -/* sysfs temp##_input -------------------------------------------------- */ - -static ssize_t thermal_temp_input_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - struct sensor_device_attribute *sensor_attr = - to_sensor_dev_attr(attr); - int idx = sensor_attr->index; - s32 value; - int res; - - res = thermal_get_sensor(idx, &value); - if (res) - return res; - if (value == TPACPI_THERMAL_SENSOR_NA) - return -ENXIO; - - return sysfs_emit(buf, "%d\n", value); -} - -#define THERMAL_SENSOR_ATTR_TEMP(_idxA, _idxB) \ - SENSOR_ATTR(temp##_idxA##_input, S_IRUGO, \ - thermal_temp_input_show, NULL, _idxB) - -static struct sensor_device_attribute sensor_dev_attr_thermal_temp_input[] = { - THERMAL_SENSOR_ATTR_TEMP(1, 0), - THERMAL_SENSOR_ATTR_TEMP(2, 1), - THERMAL_SENSOR_ATTR_TEMP(3, 2), - THERMAL_SENSOR_ATTR_TEMP(4, 3), - THERMAL_SENSOR_ATTR_TEMP(5, 4), - THERMAL_SENSOR_ATTR_TEMP(6, 5), - THERMAL_SENSOR_ATTR_TEMP(7, 6), - THERMAL_SENSOR_ATTR_TEMP(8, 7), - THERMAL_SENSOR_ATTR_TEMP(9, 8), - THERMAL_SENSOR_ATTR_TEMP(10, 9), - THERMAL_SENSOR_ATTR_TEMP(11, 10), - THERMAL_SENSOR_ATTR_TEMP(12, 11), - THERMAL_SENSOR_ATTR_TEMP(13, 12), - THERMAL_SENSOR_ATTR_TEMP(14, 13), - THERMAL_SENSOR_ATTR_TEMP(15, 14), - THERMAL_SENSOR_ATTR_TEMP(16, 15), -}; - -#define THERMAL_ATTRS(X) \ - &sensor_dev_attr_thermal_temp_input[X].dev_attr.attr - -static struct attribute *thermal_temp_input_attr[] = { - THERMAL_ATTRS(0), - THERMAL_ATTRS(1), - THERMAL_ATTRS(2), - THERMAL_ATTRS(3), - THERMAL_ATTRS(4), - THERMAL_ATTRS(5), - THERMAL_ATTRS(6), - THERMAL_ATTRS(7), - THERMAL_ATTRS(8), - THERMAL_ATTRS(9), - THERMAL_ATTRS(10), - THERMAL_ATTRS(11), - THERMAL_ATTRS(12), - THERMAL_ATTRS(13), - THERMAL_ATTRS(14), - THERMAL_ATTRS(15), - NULL -}; - -#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr) - -static umode_t thermal_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - struct device_attribute *dev_attr = to_dev_attr(attr); - struct sensor_device_attribute *sensor_attr = - to_sensor_dev_attr(dev_attr); - - int idx = sensor_attr->index; - - switch (thermal_read_mode) { - case TPACPI_THERMAL_NONE: - return 0; - - case TPACPI_THERMAL_ACPI_TMP07: - case TPACPI_THERMAL_ACPI_UPDT: - case TPACPI_THERMAL_TPEC_8: - if (idx >= 8) - return 0; - break; - - case TPACPI_THERMAL_TPEC_12: - if (idx >= 12) - return 0; - break; - - default: - break; - - } - - return attr->mode; -} - -static const struct attribute_group thermal_attr_group = { - .is_visible = thermal_attr_is_visible, - .attrs = thermal_temp_input_attr, -}; - -#undef THERMAL_SENSOR_ATTR_TEMP -#undef THERMAL_ATTRS - -static ssize_t temp1_label_show(struct device *dev, struct device_attribute *attr, char *buf) -{ - return sysfs_emit(buf, "CPU\n"); -} -static DEVICE_ATTR_RO(temp1_label); - -static ssize_t temp2_label_show(struct device *dev, struct device_attribute *attr, char *buf) -{ - return sysfs_emit(buf, "GPU\n"); -} -static DEVICE_ATTR_RO(temp2_label); - -static struct attribute *temp_label_attributes[] = { - &dev_attr_temp1_label.attr, - &dev_attr_temp2_label.attr, - NULL -}; - -static umode_t temp_label_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return thermal_use_labels ? attr->mode : 0; -} - -static const struct attribute_group temp_label_attr_group = { - .is_visible = temp_label_attr_is_visible, - .attrs = temp_label_attributes, -}; - -/* --------------------------------------------------------------------- */ - -static int __init thermal_init(struct ibm_init_struct *iibm) -{ - vdbg_printk(TPACPI_DBG_INIT, "initializing thermal subdriver\n"); - - thermal_read_mode = thermal_read_mode_check(); - - vdbg_printk(TPACPI_DBG_INIT, "thermal is %s, mode %d\n", - str_supported(thermal_read_mode != TPACPI_THERMAL_NONE), - thermal_read_mode); - - return thermal_read_mode != TPACPI_THERMAL_NONE ? 0 : -ENODEV; -} - -static int thermal_read(struct seq_file *m) -{ - int n, i; - struct ibm_thermal_sensors_struct t; - - n = thermal_get_sensors(&t); - if (unlikely(n < 0)) - return n; - - seq_printf(m, "temperatures:\t"); - - if (n > 0) { - for (i = 0; i < (n - 1); i++) - seq_printf(m, "%d ", t.temp[i] / 1000); - seq_printf(m, "%d\n", t.temp[i] / 1000); - } else - seq_printf(m, "not supported\n"); - - return 0; -} - -static struct ibm_struct thermal_driver_data = { - .name = "thermal", - .read = thermal_read, -}; - -/************************************************************************* - * Backlight/brightness subdriver - */ - -#define TPACPI_BACKLIGHT_DEV_NAME "thinkpad_screen" - -/* - * ThinkPads can read brightness from two places: EC HBRV (0x31), or - * CMOS NVRAM byte 0x5E, bits 0-3. - * - * EC HBRV (0x31) has the following layout - * Bit 7: unknown function - * Bit 6: unknown function - * Bit 5: Z: honour scale changes, NZ: ignore scale changes - * Bit 4: must be set to zero to avoid problems - * Bit 3-0: backlight brightness level - * - * brightness_get_raw returns status data in the HBRV layout - * - * WARNING: The X61 has been verified to use HBRV for something else, so - * this should be used _only_ on IBM ThinkPads, and maybe with some careful - * testing on the very early *60 Lenovo models... - */ - -enum { - TP_EC_BACKLIGHT = 0x31, - - /* TP_EC_BACKLIGHT bitmasks */ - TP_EC_BACKLIGHT_LVLMSK = 0x1F, - TP_EC_BACKLIGHT_CMDMSK = 0xE0, - TP_EC_BACKLIGHT_MAPSW = 0x20, -}; - -enum tpacpi_brightness_access_mode { - TPACPI_BRGHT_MODE_AUTO = 0, /* Not implemented yet */ - TPACPI_BRGHT_MODE_EC, /* EC control */ - TPACPI_BRGHT_MODE_UCMS_STEP, /* UCMS step-based control */ - TPACPI_BRGHT_MODE_ECNVRAM, /* EC control w/ NVRAM store */ - TPACPI_BRGHT_MODE_MAX -}; - -static struct backlight_device *ibm_backlight_device; - -static enum tpacpi_brightness_access_mode brightness_mode = - TPACPI_BRGHT_MODE_MAX; - -static unsigned int brightness_enable = 2; /* 2 = auto, 0 = no, 1 = yes */ - -static struct mutex brightness_mutex; - -/* NVRAM brightness access */ -static unsigned int tpacpi_brightness_nvram_get(void) -{ - u8 lnvram; - - lockdep_assert_held(&brightness_mutex); - - lnvram = (nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS) - & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) - >> TP_NVRAM_POS_LEVEL_BRIGHTNESS; - lnvram &= bright_maxlvl; - - return lnvram; -} - -static void tpacpi_brightness_checkpoint_nvram(void) -{ - u8 lec = 0; - u8 b_nvram; - - if (brightness_mode != TPACPI_BRGHT_MODE_ECNVRAM) - return; - - vdbg_printk(TPACPI_DBG_BRGHT, - "trying to checkpoint backlight level to NVRAM...\n"); - - if (mutex_lock_killable(&brightness_mutex) < 0) - return; - - if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) - goto unlock; - lec &= TP_EC_BACKLIGHT_LVLMSK; - b_nvram = nvram_read_byte(TP_NVRAM_ADDR_BRIGHTNESS); - - if (lec != ((b_nvram & TP_NVRAM_MASK_LEVEL_BRIGHTNESS) - >> TP_NVRAM_POS_LEVEL_BRIGHTNESS)) { - /* NVRAM needs update */ - b_nvram &= ~(TP_NVRAM_MASK_LEVEL_BRIGHTNESS << - TP_NVRAM_POS_LEVEL_BRIGHTNESS); - b_nvram |= lec; - nvram_write_byte(b_nvram, TP_NVRAM_ADDR_BRIGHTNESS); - dbg_printk(TPACPI_DBG_BRGHT, - "updated NVRAM backlight level to %u (0x%02x)\n", - (unsigned int) lec, (unsigned int) b_nvram); - } else - vdbg_printk(TPACPI_DBG_BRGHT, - "NVRAM backlight level already is %u (0x%02x)\n", - (unsigned int) lec, (unsigned int) b_nvram); - -unlock: - mutex_unlock(&brightness_mutex); -} - - -static int tpacpi_brightness_get_raw(int *status) -{ - u8 lec = 0; - - lockdep_assert_held(&brightness_mutex); - - switch (brightness_mode) { - case TPACPI_BRGHT_MODE_UCMS_STEP: - *status = tpacpi_brightness_nvram_get(); - return 0; - case TPACPI_BRGHT_MODE_EC: - case TPACPI_BRGHT_MODE_ECNVRAM: - if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) - return -EIO; - *status = lec; - return 0; - default: - return -ENXIO; - } -} - -/* do NOT call with illegal backlight level value */ -static int tpacpi_brightness_set_ec(unsigned int value) -{ - u8 lec = 0; - - lockdep_assert_held(&brightness_mutex); - - if (unlikely(!acpi_ec_read(TP_EC_BACKLIGHT, &lec))) - return -EIO; - - if (unlikely(!acpi_ec_write(TP_EC_BACKLIGHT, - (lec & TP_EC_BACKLIGHT_CMDMSK) | - (value & TP_EC_BACKLIGHT_LVLMSK)))) - return -EIO; - - return 0; -} - -static int tpacpi_brightness_set_ucmsstep(unsigned int value) -{ - int cmos_cmd, inc; - unsigned int current_value, i; - - lockdep_assert_held(&brightness_mutex); - - current_value = tpacpi_brightness_nvram_get(); - - if (value == current_value) - return 0; - - cmos_cmd = (value > current_value) ? - TP_CMOS_BRIGHTNESS_UP : - TP_CMOS_BRIGHTNESS_DOWN; - inc = (value > current_value) ? 1 : -1; - - for (i = current_value; i != value; i += inc) - if (issue_thinkpad_cmos_command(cmos_cmd)) - return -EIO; - - return 0; -} - -/* May return EINTR which can always be mapped to ERESTARTSYS */ -static int brightness_set(unsigned int value) -{ - int res; - - if (value > bright_maxlvl) - return -EINVAL; - - vdbg_printk(TPACPI_DBG_BRGHT, - "set backlight level to %d\n", value); - - res = mutex_lock_killable(&brightness_mutex); - if (res < 0) - return res; - - switch (brightness_mode) { - case TPACPI_BRGHT_MODE_EC: - case TPACPI_BRGHT_MODE_ECNVRAM: - res = tpacpi_brightness_set_ec(value); - break; - case TPACPI_BRGHT_MODE_UCMS_STEP: - res = tpacpi_brightness_set_ucmsstep(value); - break; - default: - res = -ENXIO; - } - - mutex_unlock(&brightness_mutex); - return res; -} - -/* sysfs backlight class ----------------------------------------------- */ - -static int brightness_update_status(struct backlight_device *bd) -{ - int level = backlight_get_brightness(bd); - - dbg_printk(TPACPI_DBG_BRGHT, - "backlight: attempt to set level to %d\n", - level); - - /* it is the backlight class's job (caller) to handle - * EINTR and other errors properly */ - return brightness_set(level); -} - -static int brightness_get(struct backlight_device *bd) -{ - int status, res; - - res = mutex_lock_killable(&brightness_mutex); - if (res < 0) - return 0; - - res = tpacpi_brightness_get_raw(&status); - - mutex_unlock(&brightness_mutex); - - if (res < 0) - return 0; - - return status & TP_EC_BACKLIGHT_LVLMSK; -} - -static void tpacpi_brightness_notify_change(void) -{ - backlight_force_update(ibm_backlight_device, - BACKLIGHT_UPDATE_HOTKEY); -} - -static const struct backlight_ops ibm_backlight_data = { - .get_brightness = brightness_get, - .update_status = brightness_update_status, -}; - -/* --------------------------------------------------------------------- */ - -static int __init tpacpi_evaluate_bcl(struct acpi_device *adev, void *not_used) -{ - struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; - union acpi_object *obj; - acpi_status status; - int rc; - - status = acpi_evaluate_object(adev->handle, "_BCL", NULL, &buffer); - if (ACPI_FAILURE(status)) - return 0; - - obj = buffer.pointer; - if (!obj || obj->type != ACPI_TYPE_PACKAGE) { - acpi_handle_info(adev->handle, - "Unknown _BCL data, please report this to %s\n", - TPACPI_MAIL); - rc = 0; - } else { - rc = obj->package.count; - } - kfree(obj); - - return rc; -} - -/* - * Call _BCL method of video device. On some ThinkPads this will - * switch the firmware to the ACPI brightness control mode. - */ - -static int __init tpacpi_query_bcl_levels(acpi_handle handle) -{ - struct acpi_device *device; - - device = acpi_fetch_acpi_dev(handle); - if (!device) - return 0; - - return acpi_dev_for_each_child(device, tpacpi_evaluate_bcl, NULL); -} - - -/* - * Returns 0 (no ACPI _BCL or _BCL invalid), or size of brightness map - */ -static unsigned int __init tpacpi_check_std_acpi_brightness_support(void) -{ - acpi_handle video_device; - int bcl_levels = 0; - - tpacpi_acpi_handle_locate("video", NULL, &video_device); - if (video_device) - bcl_levels = tpacpi_query_bcl_levels(video_device); - - tp_features.bright_acpimode = (bcl_levels > 0); - - return (bcl_levels > 2) ? (bcl_levels - 2) : 0; -} - -/* - * These are only useful for models that have only one possibility - * of GPU. If the BIOS model handles both ATI and Intel, don't use - * these quirks. - */ -#define TPACPI_BRGHT_Q_NOEC 0x0001 /* Must NOT use EC HBRV */ -#define TPACPI_BRGHT_Q_EC 0x0002 /* Should or must use EC HBRV */ -#define TPACPI_BRGHT_Q_ASK 0x8000 /* Ask for user report */ - -static const struct tpacpi_quirk brightness_quirk_table[] __initconst = { - /* Models with ATI GPUs known to require ECNVRAM mode */ - TPACPI_Q_IBM('1', 'Y', TPACPI_BRGHT_Q_EC), /* T43/p ATI */ - - /* Models with ATI GPUs that can use ECNVRAM */ - TPACPI_Q_IBM('1', 'R', TPACPI_BRGHT_Q_EC), /* R50,51 T40-42 */ - TPACPI_Q_IBM('1', 'Q', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), - TPACPI_Q_IBM('7', '6', TPACPI_BRGHT_Q_EC), /* R52 */ - TPACPI_Q_IBM('7', '8', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), - - /* Models with Intel Extreme Graphics 2 */ - TPACPI_Q_IBM('1', 'U', TPACPI_BRGHT_Q_NOEC), /* X40 */ - TPACPI_Q_IBM('1', 'V', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), - TPACPI_Q_IBM('1', 'W', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC), - - /* Models with Intel GMA900 */ - TPACPI_Q_IBM('7', '0', TPACPI_BRGHT_Q_NOEC), /* T43, R52 */ - TPACPI_Q_IBM('7', '4', TPACPI_BRGHT_Q_NOEC), /* X41 */ - TPACPI_Q_IBM('7', '5', TPACPI_BRGHT_Q_NOEC), /* X41 Tablet */ -}; - -/* - * Returns < 0 for error, otherwise sets tp_features.bright_* - * and bright_maxlvl. - */ -static void __init tpacpi_detect_brightness_capabilities(void) -{ - unsigned int b; - - vdbg_printk(TPACPI_DBG_INIT, - "detecting firmware brightness interface capabilities\n"); - - /* we could run a quirks check here (same table used by - * brightness_init) if needed */ - - /* - * We always attempt to detect acpi support, so as to switch - * Lenovo Vista BIOS to ACPI brightness mode even if we are not - * going to publish a backlight interface - */ - b = tpacpi_check_std_acpi_brightness_support(); - switch (b) { - case 16: - bright_maxlvl = 15; - break; - case 8: - case 0: - bright_maxlvl = 7; - break; - default: - tp_features.bright_unkfw = 1; - bright_maxlvl = b - 1; - } - pr_debug("detected %u brightness levels\n", bright_maxlvl + 1); -} - -static int __init brightness_init(struct ibm_init_struct *iibm) -{ - struct backlight_properties props; - int b; - unsigned long quirks; - - vdbg_printk(TPACPI_DBG_INIT, "initializing brightness subdriver\n"); - - mutex_init(&brightness_mutex); - - quirks = tpacpi_check_quirks(brightness_quirk_table, - ARRAY_SIZE(brightness_quirk_table)); - - /* tpacpi_detect_brightness_capabilities() must have run already */ - - /* if it is unknown, we don't handle it: it wouldn't be safe */ - if (tp_features.bright_unkfw) - return -ENODEV; - - if (!brightness_enable) { - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, - "brightness support disabled by module parameter\n"); - return -ENODEV; - } - - if (acpi_video_get_backlight_type() != acpi_backlight_vendor) { - if (brightness_enable > 1) { - pr_info("Standard ACPI backlight interface available, not loading native one\n"); - return -ENODEV; - } else if (brightness_enable == 1) { - pr_warn("Cannot enable backlight brightness support, ACPI is already handling it. Refer to the acpi_backlight kernel parameter.\n"); - return -ENODEV; - } - } else if (!tp_features.bright_acpimode) { - pr_notice("ACPI backlight interface not available\n"); - return -ENODEV; - } - - pr_notice("ACPI native brightness control enabled\n"); - - /* - * Check for module parameter bogosity, note that we - * init brightness_mode to TPACPI_BRGHT_MODE_MAX in order to be - * able to detect "unspecified" - */ - if (brightness_mode > TPACPI_BRGHT_MODE_MAX) - return -EINVAL; - - /* TPACPI_BRGHT_MODE_AUTO not implemented yet, just use default */ - if (brightness_mode == TPACPI_BRGHT_MODE_AUTO || - brightness_mode == TPACPI_BRGHT_MODE_MAX) { - if (quirks & TPACPI_BRGHT_Q_EC) - brightness_mode = TPACPI_BRGHT_MODE_ECNVRAM; - else - brightness_mode = TPACPI_BRGHT_MODE_UCMS_STEP; - - dbg_printk(TPACPI_DBG_BRGHT, - "driver auto-selected brightness_mode=%d\n", - brightness_mode); - } - - /* Safety */ - if (!tpacpi_is_ibm() && - (brightness_mode == TPACPI_BRGHT_MODE_ECNVRAM || - brightness_mode == TPACPI_BRGHT_MODE_EC)) - return -EINVAL; - - if (tpacpi_brightness_get_raw(&b) < 0) - return -ENODEV; - - memset(&props, 0, sizeof(struct backlight_properties)); - props.type = BACKLIGHT_PLATFORM; - props.max_brightness = bright_maxlvl; - props.brightness = b & TP_EC_BACKLIGHT_LVLMSK; - ibm_backlight_device = backlight_device_register(TPACPI_BACKLIGHT_DEV_NAME, - NULL, NULL, - &ibm_backlight_data, - &props); - if (IS_ERR(ibm_backlight_device)) { - int rc = PTR_ERR(ibm_backlight_device); - ibm_backlight_device = NULL; - pr_err("Could not register backlight device\n"); - return rc; - } - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, - "brightness is supported\n"); - - if (quirks & TPACPI_BRGHT_Q_ASK) { - pr_notice("brightness: will use unverified default: brightness_mode=%d\n", - brightness_mode); - pr_notice("brightness: please report to %s whether it works well or not on your ThinkPad\n", - TPACPI_MAIL); - } - - /* Added by mistake in early 2007. Probably useless, but it could - * be working around some unknown firmware problem where the value - * read at startup doesn't match the real hardware state... so leave - * it in place just in case */ - backlight_update_status(ibm_backlight_device); - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT, - "brightness: registering brightness hotkeys as change notification\n"); - tpacpi_hotkey_driver_mask_set(hotkey_driver_mask - | TP_ACPI_HKEY_BRGHTUP_MASK - | TP_ACPI_HKEY_BRGHTDWN_MASK); - return 0; -} - -static void brightness_suspend(void) -{ - tpacpi_brightness_checkpoint_nvram(); -} - -static void brightness_shutdown(void) -{ - tpacpi_brightness_checkpoint_nvram(); -} - -static void brightness_exit(void) -{ - if (ibm_backlight_device) { - vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_BRGHT, - "calling backlight_device_unregister()\n"); - backlight_device_unregister(ibm_backlight_device); - } - - tpacpi_brightness_checkpoint_nvram(); -} - -static int brightness_read(struct seq_file *m) -{ - int level; - - level = brightness_get(NULL); - if (level < 0) { - seq_printf(m, "level:\t\tunreadable\n"); - } else { - seq_printf(m, "level:\t\t%d\n", level); - seq_printf(m, "commands:\tup, down\n"); - seq_printf(m, "commands:\tlevel ( is 0-%d)\n", - bright_maxlvl); - } - - return 0; -} - -static int brightness_write(char *buf) -{ - int level; - int rc; - char *cmd; - - level = brightness_get(NULL); - if (level < 0) - return level; - - while ((cmd = strsep(&buf, ","))) { - if (strstarts(cmd, "up")) { - if (level < bright_maxlvl) - level++; - } else if (strstarts(cmd, "down")) { - if (level > 0) - level--; - } else if (sscanf(cmd, "level %d", &level) == 1 && - level >= 0 && level <= bright_maxlvl) { - /* new level set */ - } else - return -EINVAL; - } - - tpacpi_disclose_usertask("procfs brightness", - "set level to %d\n", level); - - /* - * Now we know what the final level should be, so we try to set it. - * Doing it this way makes the syscall restartable in case of EINTR - */ - rc = brightness_set(level); - if (!rc && ibm_backlight_device) - backlight_force_update(ibm_backlight_device, - BACKLIGHT_UPDATE_SYSFS); - return (rc == -EINTR) ? -ERESTARTSYS : rc; -} - -static struct ibm_struct brightness_driver_data = { - .name = "brightness", - .read = brightness_read, - .write = brightness_write, - .exit = brightness_exit, - .suspend = brightness_suspend, - .shutdown = brightness_shutdown, -}; - -/************************************************************************* - * Volume subdriver - */ - -/* - * IBM ThinkPads have a simple volume controller with MUTE gating. - * Very early Lenovo ThinkPads follow the IBM ThinkPad spec. - * - * Since the *61 series (and probably also the later *60 series), Lenovo - * ThinkPads only implement the MUTE gate. - * - * EC register 0x30 - * Bit 6: MUTE (1 mutes sound) - * Bit 3-0: Volume - * Other bits should be zero as far as we know. - * - * This is also stored in CMOS NVRAM, byte 0x60, bit 6 (MUTE), and - * bits 3-0 (volume). Other bits in NVRAM may have other functions, - * such as bit 7 which is used to detect repeated presses of MUTE, - * and we leave them unchanged. - * - * On newer Lenovo ThinkPads, the EC can automatically change the volume - * in response to user input. Unfortunately, this rarely works well. - * The laptop changes the state of its internal MUTE gate and, on some - * models, sends KEY_MUTE, causing any user code that responds to the - * mute button to get confused. The hardware MUTE gate is also - * unnecessary, since user code can handle the mute button without - * kernel or EC help. - * - * To avoid confusing userspace, we simply disable all EC-based mute - * and volume controls when possible. - */ - -#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT - -#define TPACPI_ALSA_DRVNAME "ThinkPad EC" -#define TPACPI_ALSA_SHRTNAME "ThinkPad Console Audio Control" -#define TPACPI_ALSA_MIXERNAME TPACPI_ALSA_SHRTNAME - -#if SNDRV_CARDS <= 32 -#define DEFAULT_ALSA_IDX ~((1 << (SNDRV_CARDS - 3)) - 1) -#else -#define DEFAULT_ALSA_IDX ~((1 << (32 - 3)) - 1) -#endif -static int alsa_index = DEFAULT_ALSA_IDX; /* last three slots */ -static char *alsa_id = "ThinkPadEC"; -static bool alsa_enable = SNDRV_DEFAULT_ENABLE1; - -struct tpacpi_alsa_data { - struct snd_card *card; - struct snd_ctl_elem_id *ctl_mute_id; - struct snd_ctl_elem_id *ctl_vol_id; -}; - -static struct snd_card *alsa_card; - -enum { - TP_EC_AUDIO = 0x30, - - /* TP_EC_AUDIO bits */ - TP_EC_AUDIO_MUTESW = 6, - - /* TP_EC_AUDIO bitmasks */ - TP_EC_AUDIO_LVL_MSK = 0x0F, - TP_EC_AUDIO_MUTESW_MSK = (1 << TP_EC_AUDIO_MUTESW), - - /* Maximum volume */ - TP_EC_VOLUME_MAX = 14, -}; - -enum tpacpi_volume_access_mode { - TPACPI_VOL_MODE_AUTO = 0, /* Not implemented yet */ - TPACPI_VOL_MODE_EC, /* Pure EC control */ - TPACPI_VOL_MODE_UCMS_STEP, /* UCMS step-based control: N/A */ - TPACPI_VOL_MODE_ECNVRAM, /* EC control w/ NVRAM store */ - TPACPI_VOL_MODE_MAX -}; - -enum tpacpi_volume_capabilities { - TPACPI_VOL_CAP_AUTO = 0, /* Use white/blacklist */ - TPACPI_VOL_CAP_VOLMUTE, /* Output vol and mute */ - TPACPI_VOL_CAP_MUTEONLY, /* Output mute only */ - TPACPI_VOL_CAP_MAX -}; - -enum tpacpi_mute_btn_mode { - TP_EC_MUTE_BTN_LATCH = 0, /* Mute mutes; up/down unmutes */ - /* We don't know what mode 1 is. */ - TP_EC_MUTE_BTN_NONE = 2, /* Mute and up/down are just keys */ - TP_EC_MUTE_BTN_TOGGLE = 3, /* Mute toggles; up/down unmutes */ -}; - -static enum tpacpi_volume_access_mode volume_mode = - TPACPI_VOL_MODE_MAX; - -static enum tpacpi_volume_capabilities volume_capabilities; -static bool volume_control_allowed; -static bool software_mute_requested = true; -static bool software_mute_active; -static int software_mute_orig_mode; - -/* - * Used to syncronize writers to TP_EC_AUDIO and - * TP_NVRAM_ADDR_MIXER, as we need to do read-modify-write - */ -static struct mutex volume_mutex; - -static void tpacpi_volume_checkpoint_nvram(void) -{ - u8 lec = 0; - u8 b_nvram; - u8 ec_mask; - - if (volume_mode != TPACPI_VOL_MODE_ECNVRAM) - return; - if (!volume_control_allowed) - return; - if (software_mute_active) - return; - - vdbg_printk(TPACPI_DBG_MIXER, - "trying to checkpoint mixer state to NVRAM...\n"); - - if (tp_features.mixer_no_level_control) - ec_mask = TP_EC_AUDIO_MUTESW_MSK; - else - ec_mask = TP_EC_AUDIO_MUTESW_MSK | TP_EC_AUDIO_LVL_MSK; - - if (mutex_lock_killable(&volume_mutex) < 0) - return; - - if (unlikely(!acpi_ec_read(TP_EC_AUDIO, &lec))) - goto unlock; - lec &= ec_mask; - b_nvram = nvram_read_byte(TP_NVRAM_ADDR_MIXER); - - if (lec != (b_nvram & ec_mask)) { - /* NVRAM needs update */ - b_nvram &= ~ec_mask; - b_nvram |= lec; - nvram_write_byte(b_nvram, TP_NVRAM_ADDR_MIXER); - dbg_printk(TPACPI_DBG_MIXER, - "updated NVRAM mixer status to 0x%02x (0x%02x)\n", - (unsigned int) lec, (unsigned int) b_nvram); - } else { - vdbg_printk(TPACPI_DBG_MIXER, - "NVRAM mixer status already is 0x%02x (0x%02x)\n", - (unsigned int) lec, (unsigned int) b_nvram); - } - -unlock: - mutex_unlock(&volume_mutex); -} - -static int volume_get_status_ec(u8 *status) -{ - u8 s; - - if (!acpi_ec_read(TP_EC_AUDIO, &s)) - return -EIO; - - *status = s; - - dbg_printk(TPACPI_DBG_MIXER, "status 0x%02x\n", s); - - return 0; -} - -static int volume_get_status(u8 *status) -{ - return volume_get_status_ec(status); -} - -static int volume_set_status_ec(const u8 status) -{ - if (!acpi_ec_write(TP_EC_AUDIO, status)) - return -EIO; - - dbg_printk(TPACPI_DBG_MIXER, "set EC mixer to 0x%02x\n", status); - - /* - * On X200s, and possibly on others, it can take a while for - * reads to become correct. - */ - msleep(1); - - return 0; -} - -static int volume_set_status(const u8 status) -{ - return volume_set_status_ec(status); -} - -/* returns < 0 on error, 0 on no change, 1 on change */ -static int __volume_set_mute_ec(const bool mute) -{ - int rc; - u8 s, n; - - if (mutex_lock_killable(&volume_mutex) < 0) - return -EINTR; - - rc = volume_get_status_ec(&s); - if (rc) - goto unlock; - - n = (mute) ? s | TP_EC_AUDIO_MUTESW_MSK : - s & ~TP_EC_AUDIO_MUTESW_MSK; - - if (n != s) { - rc = volume_set_status_ec(n); - if (!rc) - rc = 1; - } - -unlock: - mutex_unlock(&volume_mutex); - return rc; -} - -static int volume_alsa_set_mute(const bool mute) -{ - dbg_printk(TPACPI_DBG_MIXER, "ALSA: trying to %smute\n", - (mute) ? "" : "un"); - return __volume_set_mute_ec(mute); -} - -static int volume_set_mute(const bool mute) -{ - int rc; - - dbg_printk(TPACPI_DBG_MIXER, "trying to %smute\n", - (mute) ? "" : "un"); - - rc = __volume_set_mute_ec(mute); - return (rc < 0) ? rc : 0; -} - -/* returns < 0 on error, 0 on no change, 1 on change */ -static int __volume_set_volume_ec(const u8 vol) -{ - int rc; - u8 s, n; - - if (vol > TP_EC_VOLUME_MAX) - return -EINVAL; - - if (mutex_lock_killable(&volume_mutex) < 0) - return -EINTR; - - rc = volume_get_status_ec(&s); - if (rc) - goto unlock; - - n = (s & ~TP_EC_AUDIO_LVL_MSK) | vol; - - if (n != s) { - rc = volume_set_status_ec(n); - if (!rc) - rc = 1; - } - -unlock: - mutex_unlock(&volume_mutex); - return rc; -} - -static int volume_set_software_mute(bool startup) -{ - int result; - - if (!tpacpi_is_lenovo()) - return -ENODEV; - - if (startup) { - if (!acpi_evalf(ec_handle, &software_mute_orig_mode, - "HAUM", "qd")) - return -EIO; - - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "Initial HAUM setting was %d\n", - software_mute_orig_mode); - } - - if (!acpi_evalf(ec_handle, &result, "SAUM", "qdd", - (int)TP_EC_MUTE_BTN_NONE)) - return -EIO; - - if (result != TP_EC_MUTE_BTN_NONE) - pr_warn("Unexpected SAUM result %d\n", - result); - - /* - * In software mute mode, the standard codec controls take - * precendence, so we unmute the ThinkPad HW switch at - * startup. Just on case there are SAUM-capable ThinkPads - * with level controls, set max HW volume as well. - */ - if (tp_features.mixer_no_level_control) - result = volume_set_mute(false); - else - result = volume_set_status(TP_EC_VOLUME_MAX); - - if (result != 0) - pr_warn("Failed to unmute the HW mute switch\n"); - - return 0; -} - -static void volume_exit_software_mute(void) -{ - int r; - - if (!acpi_evalf(ec_handle, &r, "SAUM", "qdd", software_mute_orig_mode) - || r != software_mute_orig_mode) - pr_warn("Failed to restore mute mode\n"); -} - -static int volume_alsa_set_volume(const u8 vol) -{ - dbg_printk(TPACPI_DBG_MIXER, - "ALSA: trying to set volume level to %hu\n", vol); - return __volume_set_volume_ec(vol); -} - -static void volume_alsa_notify_change(void) -{ - struct tpacpi_alsa_data *d; - - if (alsa_card && alsa_card->private_data) { - d = alsa_card->private_data; - if (d->ctl_mute_id) - snd_ctl_notify(alsa_card, - SNDRV_CTL_EVENT_MASK_VALUE, - d->ctl_mute_id); - if (d->ctl_vol_id) - snd_ctl_notify(alsa_card, - SNDRV_CTL_EVENT_MASK_VALUE, - d->ctl_vol_id); - } -} - -static int volume_alsa_vol_info(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_info *uinfo) -{ - uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; - uinfo->count = 1; - uinfo->value.integer.min = 0; - uinfo->value.integer.max = TP_EC_VOLUME_MAX; - return 0; -} - -static int volume_alsa_vol_get(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - u8 s; - int rc; - - rc = volume_get_status(&s); - if (rc < 0) - return rc; - - ucontrol->value.integer.value[0] = s & TP_EC_AUDIO_LVL_MSK; - return 0; -} - -static int volume_alsa_vol_put(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - tpacpi_disclose_usertask("ALSA", "set volume to %ld\n", - ucontrol->value.integer.value[0]); - return volume_alsa_set_volume(ucontrol->value.integer.value[0]); -} - -#define volume_alsa_mute_info snd_ctl_boolean_mono_info - -static int volume_alsa_mute_get(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - u8 s; - int rc; - - rc = volume_get_status(&s); - if (rc < 0) - return rc; - - ucontrol->value.integer.value[0] = - (s & TP_EC_AUDIO_MUTESW_MSK) ? 0 : 1; - return 0; -} - -static int volume_alsa_mute_put(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - tpacpi_disclose_usertask("ALSA", "%smute\n", - ucontrol->value.integer.value[0] ? - "un" : ""); - return volume_alsa_set_mute(!ucontrol->value.integer.value[0]); -} - -static struct snd_kcontrol_new volume_alsa_control_vol __initdata = { - .iface = SNDRV_CTL_ELEM_IFACE_MIXER, - .name = "Console Playback Volume", - .index = 0, - .access = SNDRV_CTL_ELEM_ACCESS_READ, - .info = volume_alsa_vol_info, - .get = volume_alsa_vol_get, -}; - -static struct snd_kcontrol_new volume_alsa_control_mute __initdata = { - .iface = SNDRV_CTL_ELEM_IFACE_MIXER, - .name = "Console Playback Switch", - .index = 0, - .access = SNDRV_CTL_ELEM_ACCESS_READ, - .info = volume_alsa_mute_info, - .get = volume_alsa_mute_get, -}; - -static void volume_suspend(void) -{ - tpacpi_volume_checkpoint_nvram(); -} - -static void volume_resume(void) -{ - if (software_mute_active) { - if (volume_set_software_mute(false) < 0) - pr_warn("Failed to restore software mute\n"); - } else { - volume_alsa_notify_change(); - } -} - -static void volume_shutdown(void) -{ - tpacpi_volume_checkpoint_nvram(); -} - -static void volume_exit(void) -{ - if (alsa_card) { - snd_card_free(alsa_card); - alsa_card = NULL; - } - - tpacpi_volume_checkpoint_nvram(); - - if (software_mute_active) - volume_exit_software_mute(); -} - -static int __init volume_create_alsa_mixer(void) -{ - struct snd_card *card; - struct tpacpi_alsa_data *data; - struct snd_kcontrol *ctl_vol; - struct snd_kcontrol *ctl_mute; - int rc; - - rc = snd_card_new(&tpacpi_pdev->dev, - alsa_index, alsa_id, THIS_MODULE, - sizeof(struct tpacpi_alsa_data), &card); - if (rc < 0 || !card) { - pr_err("Failed to create ALSA card structures: %d\n", rc); - return -ENODEV; - } - - BUG_ON(!card->private_data); - data = card->private_data; - data->card = card; - - strscpy(card->driver, TPACPI_ALSA_DRVNAME); - strscpy(card->shortname, TPACPI_ALSA_SHRTNAME); - snprintf(card->mixername, sizeof(card->mixername), "ThinkPad EC %s", - (thinkpad_id.ec_version_str) ? - thinkpad_id.ec_version_str : "(unknown)"); - snprintf(card->longname, sizeof(card->longname), - "%s at EC reg 0x%02x, fw %s", card->shortname, TP_EC_AUDIO, - (thinkpad_id.ec_version_str) ? - thinkpad_id.ec_version_str : "unknown"); - - if (volume_control_allowed) { - volume_alsa_control_vol.put = volume_alsa_vol_put; - volume_alsa_control_vol.access = - SNDRV_CTL_ELEM_ACCESS_READWRITE; - - volume_alsa_control_mute.put = volume_alsa_mute_put; - volume_alsa_control_mute.access = - SNDRV_CTL_ELEM_ACCESS_READWRITE; - } - - if (!tp_features.mixer_no_level_control) { - ctl_vol = snd_ctl_new1(&volume_alsa_control_vol, NULL); - rc = snd_ctl_add(card, ctl_vol); - if (rc < 0) { - pr_err("Failed to create ALSA volume control: %d\n", - rc); - goto err_exit; - } - data->ctl_vol_id = &ctl_vol->id; - } - - ctl_mute = snd_ctl_new1(&volume_alsa_control_mute, NULL); - rc = snd_ctl_add(card, ctl_mute); - if (rc < 0) { - pr_err("Failed to create ALSA mute control: %d\n", rc); - goto err_exit; - } - data->ctl_mute_id = &ctl_mute->id; - - rc = snd_card_register(card); - if (rc < 0) { - pr_err("Failed to register ALSA card: %d\n", rc); - goto err_exit; - } - - alsa_card = card; - return 0; - -err_exit: - snd_card_free(card); - return -ENODEV; -} - -#define TPACPI_VOL_Q_MUTEONLY 0x0001 /* Mute-only control available */ -#define TPACPI_VOL_Q_LEVEL 0x0002 /* Volume control available */ - -static const struct tpacpi_quirk volume_quirk_table[] __initconst = { - /* Whitelist volume level on all IBM by default */ - { .vendor = PCI_VENDOR_ID_IBM, - .bios = TPACPI_MATCH_ANY, - .ec = TPACPI_MATCH_ANY, - .quirks = TPACPI_VOL_Q_LEVEL }, - - /* Lenovo models with volume control (needs confirmation) */ - TPACPI_QEC_LNV('7', 'C', TPACPI_VOL_Q_LEVEL), /* R60/i */ - TPACPI_QEC_LNV('7', 'E', TPACPI_VOL_Q_LEVEL), /* R60e/i */ - TPACPI_QEC_LNV('7', '9', TPACPI_VOL_Q_LEVEL), /* T60/p */ - TPACPI_QEC_LNV('7', 'B', TPACPI_VOL_Q_LEVEL), /* X60/s */ - TPACPI_QEC_LNV('7', 'J', TPACPI_VOL_Q_LEVEL), /* X60t */ - TPACPI_QEC_LNV('7', '7', TPACPI_VOL_Q_LEVEL), /* Z60 */ - TPACPI_QEC_LNV('7', 'F', TPACPI_VOL_Q_LEVEL), /* Z61 */ - - /* Whitelist mute-only on all Lenovo by default */ - { .vendor = PCI_VENDOR_ID_LENOVO, - .bios = TPACPI_MATCH_ANY, - .ec = TPACPI_MATCH_ANY, - .quirks = TPACPI_VOL_Q_MUTEONLY } -}; - -static int __init volume_init(struct ibm_init_struct *iibm) -{ - unsigned long quirks; - int rc; - - vdbg_printk(TPACPI_DBG_INIT, "initializing volume subdriver\n"); - - mutex_init(&volume_mutex); - - /* - * Check for module parameter bogosity, note that we - * init volume_mode to TPACPI_VOL_MODE_MAX in order to be - * able to detect "unspecified" - */ - if (volume_mode > TPACPI_VOL_MODE_MAX) - return -EINVAL; - - if (volume_mode == TPACPI_VOL_MODE_UCMS_STEP) { - pr_err("UCMS step volume mode not implemented, please contact %s\n", - TPACPI_MAIL); - return -ENODEV; - } - - if (volume_capabilities >= TPACPI_VOL_CAP_MAX) - return -EINVAL; - - /* - * The ALSA mixer is our primary interface. - * When disabled, don't install the subdriver at all - */ - if (!alsa_enable) { - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "ALSA mixer disabled by parameter, not loading volume subdriver...\n"); - return -ENODEV; - } - - quirks = tpacpi_check_quirks(volume_quirk_table, - ARRAY_SIZE(volume_quirk_table)); - - switch (volume_capabilities) { - case TPACPI_VOL_CAP_AUTO: - if (quirks & TPACPI_VOL_Q_MUTEONLY) - tp_features.mixer_no_level_control = 1; - else if (quirks & TPACPI_VOL_Q_LEVEL) - tp_features.mixer_no_level_control = 0; - else - return -ENODEV; /* no mixer */ - break; - case TPACPI_VOL_CAP_VOLMUTE: - tp_features.mixer_no_level_control = 0; - break; - case TPACPI_VOL_CAP_MUTEONLY: - tp_features.mixer_no_level_control = 1; - break; - default: - return -ENODEV; - } - - if (volume_capabilities != TPACPI_VOL_CAP_AUTO) - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "using user-supplied volume_capabilities=%d\n", - volume_capabilities); - - if (volume_mode == TPACPI_VOL_MODE_AUTO || - volume_mode == TPACPI_VOL_MODE_MAX) { - volume_mode = TPACPI_VOL_MODE_ECNVRAM; - - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "driver auto-selected volume_mode=%d\n", - volume_mode); - } else { - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "using user-supplied volume_mode=%d\n", - volume_mode); - } - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "mute is supported, volume control is %s\n", - str_supported(!tp_features.mixer_no_level_control)); - - if (software_mute_requested && volume_set_software_mute(true) == 0) { - software_mute_active = true; - } else { - rc = volume_create_alsa_mixer(); - if (rc) { - pr_err("Could not create the ALSA mixer interface\n"); - return rc; - } - - pr_info("Console audio control enabled, mode: %s\n", - (volume_control_allowed) ? - "override (read/write)" : - "monitor (read only)"); - } - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER, - "registering volume hotkeys as change notification\n"); - tpacpi_hotkey_driver_mask_set(hotkey_driver_mask - | TP_ACPI_HKEY_VOLUP_MASK - | TP_ACPI_HKEY_VOLDWN_MASK - | TP_ACPI_HKEY_MUTE_MASK); - - return 0; -} - -static int volume_read(struct seq_file *m) -{ - u8 status; - - if (volume_get_status(&status) < 0) { - seq_printf(m, "level:\t\tunreadable\n"); - } else { - if (tp_features.mixer_no_level_control) - seq_printf(m, "level:\t\tunsupported\n"); - else - seq_printf(m, "level:\t\t%d\n", - status & TP_EC_AUDIO_LVL_MSK); - - seq_printf(m, "mute:\t\t%s\n", str_on_off(status & BIT(TP_EC_AUDIO_MUTESW))); - - if (volume_control_allowed) { - seq_printf(m, "commands:\tunmute, mute\n"); - if (!tp_features.mixer_no_level_control) { - seq_printf(m, "commands:\tup, down\n"); - seq_printf(m, "commands:\tlevel ( is 0-%d)\n", - TP_EC_VOLUME_MAX); - } - } - } - - return 0; -} - -static int volume_write(char *buf) -{ - u8 s; - u8 new_level, new_mute; - int l; - char *cmd; - int rc; - - /* - * We do allow volume control at driver startup, so that the - * user can set initial state through the volume=... parameter hack. - */ - if (!volume_control_allowed && tpacpi_lifecycle != TPACPI_LIFE_INIT) { - if (unlikely(!tp_warned.volume_ctrl_forbidden)) { - tp_warned.volume_ctrl_forbidden = 1; - pr_notice("Console audio control in monitor mode, changes are not allowed\n"); - pr_notice("Use the volume_control=1 module parameter to enable volume control\n"); - } - return -EPERM; - } - - rc = volume_get_status(&s); - if (rc < 0) - return rc; - - new_level = s & TP_EC_AUDIO_LVL_MSK; - new_mute = s & TP_EC_AUDIO_MUTESW_MSK; - - while ((cmd = strsep(&buf, ","))) { - if (!tp_features.mixer_no_level_control) { - if (strstarts(cmd, "up")) { - if (new_mute) - new_mute = 0; - else if (new_level < TP_EC_VOLUME_MAX) - new_level++; - continue; - } else if (strstarts(cmd, "down")) { - if (new_mute) - new_mute = 0; - else if (new_level > 0) - new_level--; - continue; - } else if (sscanf(cmd, "level %u", &l) == 1 && - l >= 0 && l <= TP_EC_VOLUME_MAX) { - new_level = l; - continue; - } - } - if (strstarts(cmd, "mute")) - new_mute = TP_EC_AUDIO_MUTESW_MSK; - else if (strstarts(cmd, "unmute")) - new_mute = 0; - else - return -EINVAL; - } - - if (tp_features.mixer_no_level_control) { - tpacpi_disclose_usertask("procfs volume", "%smute\n", - new_mute ? "" : "un"); - rc = volume_set_mute(!!new_mute); - } else { - tpacpi_disclose_usertask("procfs volume", - "%smute and set level to %d\n", - new_mute ? "" : "un", new_level); - rc = volume_set_status(new_mute | new_level); - } - volume_alsa_notify_change(); - - return (rc == -EINTR) ? -ERESTARTSYS : rc; -} - -static struct ibm_struct volume_driver_data = { - .name = "volume", - .read = volume_read, - .write = volume_write, - .exit = volume_exit, - .suspend = volume_suspend, - .resume = volume_resume, - .shutdown = volume_shutdown, -}; - -#else /* !CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ - -#define alsa_card NULL - -static inline void volume_alsa_notify_change(void) -{ -} - -static int __init volume_init(struct ibm_init_struct *iibm) -{ - pr_info("volume: disabled as there is no ALSA support in this kernel\n"); - - return -ENODEV; -} - -static struct ibm_struct volume_driver_data = { - .name = "volume", -}; - -#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ - -/************************************************************************* - * Fan subdriver - */ - -/* - * FAN ACCESS MODES - * - * TPACPI_FAN_RD_ACPI_GFAN: - * ACPI GFAN method: returns fan level - * - * see TPACPI_FAN_WR_ACPI_SFAN - * EC 0x2f (HFSP) not available if GFAN exists - * - * TPACPI_FAN_WR_ACPI_SFAN: - * ACPI SFAN method: sets fan level, 0 (stop) to 7 (max) - * - * EC 0x2f (HFSP) might be available *for reading*, but do not use - * it for writing. - * - * TPACPI_FAN_RD_ACPI_FANG: - * ACPI FANG method: returns fan control register - * - * Takes one parameter which is 0x8100 plus the offset to EC memory - * address 0xf500 and returns the byte at this address. - * - * 0xf500: - * When the value is less than 9 automatic mode is enabled - * 0xf502: - * Contains the current fan speed from 0-100% - * 0xf506: - * Bit 7 has to be set in order to enable manual control by - * writing a value >= 9 to 0xf500 - * - * TPACPI_FAN_WR_ACPI_FANW: - * ACPI FANW method: sets fan control registers - * - * Takes 0x8100 plus the offset to EC memory address 0xf500 and the - * value to be written there as parameters. - * - * see TPACPI_FAN_RD_ACPI_FANG - * - * TPACPI_FAN_WR_TPEC: - * ThinkPad EC register 0x2f (HFSP): fan control loop mode - * Supported on almost all ThinkPads - * - * Fan speed changes of any sort (including those caused by the - * disengaged mode) are usually done slowly by the firmware as the - * maximum amount of fan duty cycle change per second seems to be - * limited. - * - * Reading is not available if GFAN exists. - * Writing is not available if SFAN exists. - * - * Bits - * 7 automatic mode engaged; - * (default operation mode of the ThinkPad) - * fan level is ignored in this mode. - * 6 full speed mode (takes precedence over bit 7); - * not available on all thinkpads. May disable - * the tachometer while the fan controller ramps up - * the speed (which can take up to a few *minutes*). - * Speeds up fan to 100% duty-cycle, which is far above - * the standard RPM levels. It is not impossible that - * it could cause hardware damage. - * 5-3 unused in some models. Extra bits for fan level - * in others, but still useless as all values above - * 7 map to the same speed as level 7 in these models. - * 2-0 fan level (0..7 usually) - * 0x00 = stop - * 0x07 = max (set when temperatures critical) - * Some ThinkPads may have other levels, see - * TPACPI_FAN_WR_ACPI_FANS (X31/X40/X41) - * - * FIRMWARE BUG: on some models, EC 0x2f might not be initialized at - * boot. Apparently the EC does not initialize it, so unless ACPI DSDT - * does so, its initial value is meaningless (0x07). - * - * For firmware bugs, refer to: - * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues - * - * ---- - * - * ThinkPad EC register 0x84 (LSB), 0x85 (MSB): - * Main fan tachometer reading (in RPM) - * - * This register is present on all ThinkPads with a new-style EC, and - * it is known not to be present on the A21m/e, and T22, as there is - * something else in offset 0x84 according to the ACPI DSDT. Other - * ThinkPads from this same time period (and earlier) probably lack the - * tachometer as well. - * - * Unfortunately a lot of ThinkPads with new-style ECs but whose firmware - * was never fixed by IBM to report the EC firmware version string - * probably support the tachometer (like the early X models), so - * detecting it is quite hard. We need more data to know for sure. - * - * FIRMWARE BUG: always read 0x84 first, otherwise incorrect readings - * might result. - * - * FIRMWARE BUG: may go stale while the EC is switching to full speed - * mode. - * - * For firmware bugs, refer to: - * https://thinkwiki.org/wiki/Embedded_Controller_Firmware#Firmware_Issues - * - * ---- - * - * ThinkPad EC register 0x31 bit 0 (only on select models) - * - * When bit 0 of EC register 0x31 is zero, the tachometer registers - * show the speed of the main fan. When bit 0 of EC register 0x31 - * is one, the tachometer registers show the speed of the auxiliary - * fan. - * - * Fan control seems to affect both fans, regardless of the state - * of this bit. - * - * So far, only the firmware for the X60/X61 non-tablet versions - * seem to support this (firmware TP-7M). - * - * TPACPI_FAN_WR_ACPI_FANS: - * ThinkPad X31, X40, X41. Not available in the X60. - * - * FANS ACPI handle: takes three arguments: low speed, medium speed, - * high speed. ACPI DSDT seems to map these three speeds to levels - * as follows: STOP LOW LOW MED MED HIGH HIGH HIGH HIGH - * (this map is stored on FAN0..FAN8 as "0,1,1,2,2,3,3,3,3") - * - * The speeds are stored on handles - * (FANA:FAN9), (FANC:FANB), (FANE:FAND). - * - * There are three default speed sets, accessible as handles: - * FS1L,FS1M,FS1H; FS2L,FS2M,FS2H; FS3L,FS3M,FS3H - * - * ACPI DSDT switches which set is in use depending on various - * factors. - * - * TPACPI_FAN_WR_TPEC is also available and should be used to - * command the fan. The X31/X40/X41 seems to have 8 fan levels, - * but the ACPI tables just mention level 7. - * - * TPACPI_FAN_RD_TPEC_NS: - * This mode is used for a few ThinkPads (L13 Yoga Gen2, X13 Yoga Gen2 etc.) - * that are using non-standard EC locations for reporting fan speeds. - * Currently these platforms only provide fan rpm reporting. - * - */ - -#define FAN_RPM_CAL_CONST 491520 /* FAN RPM calculation offset for some non-standard ECFW */ - -#define FAN_NS_CTRL_STATUS BIT(2) /* Bit which determines control is enabled or not */ -#define FAN_NS_CTRL BIT(4) /* Bit which determines control is by host or EC */ -#define FAN_CLOCK_TPM (22500*60) /* Ticks per minute for a 22.5 kHz clock */ - -enum { /* Fan control constants */ - fan_status_offset = 0x2f, /* EC register 0x2f */ - fan_rpm_offset = 0x84, /* EC register 0x84: LSB, 0x85 MSB (RPM) - * 0x84 must be read before 0x85 */ - fan_select_offset = 0x31, /* EC register 0x31 (Firmware 7M) - bit 0 selects which fan is active */ - - fan_status_offset_ns = 0x93, /* Special status/control offset for non-standard EC Fan1 */ - fan2_status_offset_ns = 0x96, /* Special status/control offset for non-standard EC Fan2 */ - fan_rpm_status_ns = 0x95, /* Special offset for Fan1 RPM status for non-standard EC */ - fan2_rpm_status_ns = 0x98, /* Special offset for Fan2 RPM status for non-standard EC */ - - TP_EC_FAN_FULLSPEED = 0x40, /* EC fan mode: full speed */ - TP_EC_FAN_AUTO = 0x80, /* EC fan mode: auto fan control */ - - TPACPI_FAN_LAST_LEVEL = 0x100, /* Use cached last-seen fan level */ -}; - -enum fan_status_access_mode { - TPACPI_FAN_NONE = 0, /* No fan status or control */ - TPACPI_FAN_RD_ACPI_GFAN, /* Use ACPI GFAN */ - TPACPI_FAN_RD_ACPI_FANG, /* Use ACPI FANG */ - TPACPI_FAN_RD_TPEC, /* Use ACPI EC regs 0x2f, 0x84-0x85 */ - TPACPI_FAN_RD_TPEC_NS, /* Use non-standard ACPI EC regs (eg: L13 Yoga gen2 etc.) */ -}; - -enum fan_control_access_mode { - TPACPI_FAN_WR_NONE = 0, /* No fan control */ - TPACPI_FAN_WR_ACPI_SFAN, /* Use ACPI SFAN */ - TPACPI_FAN_WR_ACPI_FANW, /* Use ACPI FANW */ - TPACPI_FAN_WR_TPEC, /* Use ACPI EC reg 0x2f */ - TPACPI_FAN_WR_ACPI_FANS, /* Use ACPI FANS and EC reg 0x2f */ -}; - -enum fan_control_commands { - TPACPI_FAN_CMD_SPEED = 0x0001, /* speed command */ - TPACPI_FAN_CMD_LEVEL = 0x0002, /* level command */ - TPACPI_FAN_CMD_ENABLE = 0x0004, /* enable/disable cmd, - * and also watchdog cmd */ -}; - -static bool fan_control_allowed; - -static enum fan_status_access_mode fan_status_access_mode; -static enum fan_control_access_mode fan_control_access_mode; -static enum fan_control_commands fan_control_commands; - -static u8 fan_control_initial_status; -static u8 fan_control_desired_level; -static u8 fan_control_resume_level; -static int fan_watchdog_maxinterval; - -static bool fan_with_ns_addr; -static bool ecfw_with_fan_dec_rpm; -static bool fan_speed_in_tpr; - -static struct mutex fan_mutex; - -static void fan_watchdog_fire(struct work_struct *ignored); -static DECLARE_DELAYED_WORK(fan_watchdog_task, fan_watchdog_fire); - -TPACPI_HANDLE(fans, ec, "FANS"); /* X31, X40, X41 */ -TPACPI_HANDLE(gfan, ec, "GFAN", /* 570 */ - "\\FSPD", /* 600e/x, 770e, 770x */ - ); /* all others */ -TPACPI_HANDLE(fang, ec, "FANG", /* E531 */ - ); /* all others */ -TPACPI_HANDLE(sfan, ec, "SFAN", /* 570 */ - "JFNS", /* 770x-JL */ - ); /* all others */ -TPACPI_HANDLE(fanw, ec, "FANW", /* E531 */ - ); /* all others */ - -/* - * Unitialized HFSP quirk: ACPI DSDT and EC fail to initialize the - * HFSP register at boot, so it contains 0x07 but the Thinkpad could - * be in auto mode (0x80). - * - * This is corrected by any write to HFSP either by the driver, or - * by the firmware. - * - * We assume 0x07 really means auto mode while this quirk is active, - * as this is far more likely than the ThinkPad being in level 7, - * which is only used by the firmware during thermal emergencies. - * - * Enable for TP-1Y (T43), TP-78 (R51e), TP-76 (R52), - * TP-70 (T43, R52), which are known to be buggy. - */ - -static void fan_quirk1_setup(void) -{ - if (fan_control_initial_status == 0x07) { - pr_notice("fan_init: initial fan status is unknown, assuming it is in auto mode\n"); - tp_features.fan_ctrl_status_undef = 1; - } -} - -static void fan_quirk1_handle(u8 *fan_status) -{ - if (unlikely(tp_features.fan_ctrl_status_undef)) { - if (*fan_status != fan_control_initial_status) { - /* something changed the HFSP regisnter since - * driver init time, so it is not undefined - * anymore */ - tp_features.fan_ctrl_status_undef = 0; - } else { - /* Return most likely status. In fact, it - * might be the only possible status */ - *fan_status = TP_EC_FAN_AUTO; - } - } -} - -/* Select main fan on X60/X61, NOOP on others */ -static bool fan_select_fan1(void) -{ - if (tp_features.second_fan) { - u8 val; - - if (ec_read(fan_select_offset, &val) < 0) - return false; - val &= 0xFEU; - if (ec_write(fan_select_offset, val) < 0) - return false; - } - return true; -} - -/* Select secondary fan on X60/X61 */ -static bool fan_select_fan2(void) -{ - u8 val; - - if (!tp_features.second_fan) - return false; - - if (ec_read(fan_select_offset, &val) < 0) - return false; - val |= 0x01U; - if (ec_write(fan_select_offset, val) < 0) - return false; - - return true; -} - -static void fan_update_desired_level(u8 status) -{ - lockdep_assert_held(&fan_mutex); - - if ((status & - (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { - if (status > 7) - fan_control_desired_level = 7; - else - fan_control_desired_level = status; - } -} - -static int fan_get_status(u8 *status) -{ - u8 s; - - /* TODO: - * Add TPACPI_FAN_RD_ACPI_FANS ? */ - - switch (fan_status_access_mode) { - case TPACPI_FAN_RD_ACPI_GFAN: { - /* 570, 600e/x, 770e, 770x */ - int res; - - if (unlikely(!acpi_evalf(gfan_handle, &res, NULL, "d"))) - return -EIO; - - if (likely(status)) - *status = res & 0x07; - - break; - } - case TPACPI_FAN_RD_ACPI_FANG: { - /* E531 */ - int mode, speed; - - if (unlikely(!acpi_evalf(fang_handle, &mode, NULL, "dd", 0x8100))) - return -EIO; - if (unlikely(!acpi_evalf(fang_handle, &speed, NULL, "dd", 0x8102))) - return -EIO; - - if (likely(status)) { - *status = speed * 7 / 100; - if (mode < 9) - *status |= TP_EC_FAN_AUTO; - } - - break; - } - case TPACPI_FAN_RD_TPEC: - /* all except 570, 600e/x, 770e, 770x */ - if (unlikely(!acpi_ec_read(fan_status_offset, &s))) - return -EIO; - - if (likely(status)) { - *status = s; - fan_quirk1_handle(status); - } - - break; - case TPACPI_FAN_RD_TPEC_NS: - /* Default mode is AUTO which means controlled by EC */ - if (!acpi_ec_read(fan_status_offset_ns, &s)) - return -EIO; - - if (status) - *status = s; - - break; - - default: - return -ENXIO; - } - - return 0; -} - -static int fan_get_status_safe(u8 *status) -{ - int rc; - u8 s; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - rc = fan_get_status(&s); - /* NS EC doesn't have register with level settings */ - if (!rc && !fan_with_ns_addr) - fan_update_desired_level(s); - mutex_unlock(&fan_mutex); - - if (rc) - return rc; - if (status) - *status = s; - - return 0; -} - -static int fan_get_speed(unsigned int *speed) -{ - u8 hi, lo; - - switch (fan_status_access_mode) { - case TPACPI_FAN_RD_TPEC: - /* all except 570, 600e/x, 770e, 770x */ - if (unlikely(!fan_select_fan1())) - return -EIO; - if (unlikely(!acpi_ec_read(fan_rpm_offset, &lo) || - !acpi_ec_read(fan_rpm_offset + 1, &hi))) - return -EIO; - - if (likely(speed)) { - *speed = (hi << 8) | lo; - if (fan_speed_in_tpr && *speed != 0) - *speed = FAN_CLOCK_TPM / *speed; - } - break; - case TPACPI_FAN_RD_TPEC_NS: - if (!acpi_ec_read(fan_rpm_status_ns, &lo)) - return -EIO; - - if (speed) - *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; - break; - - default: - return -ENXIO; - } - - return 0; -} - -static int fan2_get_speed(unsigned int *speed) -{ - u8 hi, lo, status; - bool rc; - - switch (fan_status_access_mode) { - case TPACPI_FAN_RD_TPEC: - /* all except 570, 600e/x, 770e, 770x */ - if (unlikely(!fan_select_fan2())) - return -EIO; - rc = !acpi_ec_read(fan_rpm_offset, &lo) || - !acpi_ec_read(fan_rpm_offset + 1, &hi); - fan_select_fan1(); /* play it safe */ - if (rc) - return -EIO; - - if (likely(speed)) { - *speed = (hi << 8) | lo; - if (fan_speed_in_tpr && *speed != 0) - *speed = FAN_CLOCK_TPM / *speed; - } - break; - - case TPACPI_FAN_RD_TPEC_NS: - rc = !acpi_ec_read(fan2_status_offset_ns, &status); - if (rc) - return -EIO; - if (!(status & FAN_NS_CTRL_STATUS)) { - pr_info("secondary fan control not supported\n"); - return -EIO; - } - rc = !acpi_ec_read(fan2_rpm_status_ns, &lo); - if (rc) - return -EIO; - if (speed) - *speed = lo ? FAN_RPM_CAL_CONST / lo : 0; - break; - case TPACPI_FAN_RD_ACPI_FANG: { - /* E531 */ - int speed_tmp; - - if (unlikely(!acpi_evalf(fang_handle, &speed_tmp, NULL, "dd", 0x8102))) - return -EIO; - - if (likely(speed)) - *speed = speed_tmp * 65535 / 100; - break; - } - - default: - return -ENXIO; - } - - return 0; -} - -static int fan_set_level(int level) -{ - if (!fan_control_allowed) - return -EPERM; - - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_SFAN: - if ((level < 0) || (level > 7)) - return -EINVAL; - - if (tp_features.second_fan_ctl) { - if (!fan_select_fan2() || - !acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) { - pr_warn("Couldn't set 2nd fan level, disabling support\n"); - tp_features.second_fan_ctl = 0; - } - fan_select_fan1(); - } - if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", level)) - return -EIO; - break; - - case TPACPI_FAN_WR_ACPI_FANS: - case TPACPI_FAN_WR_TPEC: - if (!(level & TP_EC_FAN_AUTO) && - !(level & TP_EC_FAN_FULLSPEED) && - ((level < 0) || (level > 7))) - return -EINVAL; - - /* safety net should the EC not support AUTO - * or FULLSPEED mode bits and just ignore them */ - if (level & TP_EC_FAN_FULLSPEED) - level |= 7; /* safety min speed 7 */ - else if (level & TP_EC_FAN_AUTO) - level |= 4; /* safety min speed 4 */ - - if (tp_features.second_fan_ctl) { - if (!fan_select_fan2() || - !acpi_ec_write(fan_status_offset, level)) { - pr_warn("Couldn't set 2nd fan level, disabling support\n"); - tp_features.second_fan_ctl = 0; - } - fan_select_fan1(); - - } - if (!acpi_ec_write(fan_status_offset, level)) - return -EIO; - else - tp_features.fan_ctrl_status_undef = 0; - break; - - case TPACPI_FAN_WR_ACPI_FANW: - if (!(level & TP_EC_FAN_AUTO) && (level < 0 || level > 7)) - return -EINVAL; - if (level & TP_EC_FAN_FULLSPEED) - return -EINVAL; - - if (level & TP_EC_FAN_AUTO) { - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { - return -EIO; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { - return -EIO; - } - } else { - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { - return -EIO; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { - return -EIO; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, level * 100 / 7)) { - return -EIO; - } - } - break; - - default: - return -ENXIO; - } - - vdbg_printk(TPACPI_DBG_FAN, - "fan control: set fan control register to 0x%02x\n", level); - return 0; -} - -static int fan_set_level_safe(int level) -{ - int rc; - - if (!fan_control_allowed) - return -EPERM; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - - if (level == TPACPI_FAN_LAST_LEVEL) - level = fan_control_desired_level; - - rc = fan_set_level(level); - if (!rc) - fan_update_desired_level(level); - - mutex_unlock(&fan_mutex); - return rc; -} - -static int fan_set_enable(void) -{ - u8 s = 0; - int rc; - - if (!fan_control_allowed) - return -EPERM; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_FANS: - case TPACPI_FAN_WR_TPEC: - rc = fan_get_status(&s); - if (rc) - break; - - /* Don't go out of emergency fan mode */ - if (s != 7) { - s &= 0x07; - s |= TP_EC_FAN_AUTO | 4; /* min fan speed 4 */ - } - - if (!acpi_ec_write(fan_status_offset, s)) - rc = -EIO; - else { - tp_features.fan_ctrl_status_undef = 0; - rc = 0; - } - break; - - case TPACPI_FAN_WR_ACPI_SFAN: - rc = fan_get_status(&s); - if (rc) - break; - - s &= 0x07; - - /* Set fan to at least level 4 */ - s |= 4; - - if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", s)) - rc = -EIO; - else - rc = 0; - break; - - case TPACPI_FAN_WR_ACPI_FANW: - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x05)) { - rc = -EIO; - break; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0x00)) { - rc = -EIO; - break; - } - - rc = 0; - break; - - default: - rc = -ENXIO; - } - - mutex_unlock(&fan_mutex); - - if (!rc) - vdbg_printk(TPACPI_DBG_FAN, - "fan control: set fan control register to 0x%02x\n", - s); - return rc; -} - -static int fan_set_disable(void) -{ - int rc; - - if (!fan_control_allowed) - return -EPERM; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - - rc = 0; - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_FANS: - case TPACPI_FAN_WR_TPEC: - if (!acpi_ec_write(fan_status_offset, 0x00)) - rc = -EIO; - else { - fan_control_desired_level = 0; - tp_features.fan_ctrl_status_undef = 0; - } - break; - - case TPACPI_FAN_WR_ACPI_SFAN: - if (!acpi_evalf(sfan_handle, NULL, NULL, "vd", 0x00)) - rc = -EIO; - else - fan_control_desired_level = 0; - break; - - case TPACPI_FAN_WR_ACPI_FANW: - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { - rc = -EIO; - break; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { - rc = -EIO; - break; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8102, 0x00)) { - rc = -EIO; - break; - } - rc = 0; - break; - - default: - rc = -ENXIO; - } - - if (!rc) - vdbg_printk(TPACPI_DBG_FAN, - "fan control: set fan control register to 0\n"); - - mutex_unlock(&fan_mutex); - return rc; -} - -static int fan_set_speed(int speed) -{ - int rc; - - if (!fan_control_allowed) - return -EPERM; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - - rc = 0; - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_FANS: - if (speed >= 0 && speed <= 65535) { - if (!acpi_evalf(fans_handle, NULL, NULL, "vddd", - speed, speed, speed)) - rc = -EIO; - } else - rc = -EINVAL; - break; - - case TPACPI_FAN_WR_ACPI_FANW: - if (speed >= 0 && speed <= 65535) { - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8106, 0x45)) { - rc = -EIO; - break; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", 0x8100, 0xff)) { - rc = -EIO; - break; - } - if (!acpi_evalf(fanw_handle, NULL, NULL, "vdd", - 0x8102, speed * 100 / 65535)) - rc = -EIO; - } else - rc = -EINVAL; - break; - - default: - rc = -ENXIO; - } - - mutex_unlock(&fan_mutex); - return rc; -} - -static void fan_watchdog_reset(void) -{ - if (fan_control_access_mode == TPACPI_FAN_WR_NONE) - return; - - if (fan_watchdog_maxinterval > 0 && - tpacpi_lifecycle != TPACPI_LIFE_EXITING) - mod_delayed_work(tpacpi_wq, &fan_watchdog_task, - secs_to_jiffies(fan_watchdog_maxinterval)); - else - cancel_delayed_work(&fan_watchdog_task); -} - -static void fan_watchdog_fire(struct work_struct *ignored) -{ - int rc; - - if (tpacpi_lifecycle != TPACPI_LIFE_RUNNING) - return; - - pr_notice("fan watchdog: enabling fan\n"); - rc = fan_set_enable(); - if (rc < 0) { - pr_err("fan watchdog: error %d while enabling fan, will try again later...\n", - rc); - /* reschedule for later */ - fan_watchdog_reset(); - } -} - -/* - * SYSFS fan layout: hwmon compatible (device) - * - * pwm*_enable: - * 0: "disengaged" mode - * 1: manual mode - * 2: native EC "auto" mode (recommended, hardware default) - * - * pwm*: set speed in manual mode, ignored otherwise. - * 0 is level 0; 255 is level 7. Intermediate points done with linear - * interpolation. - * - * fan*_input: tachometer reading, RPM - * - * - * SYSFS fan layout: extensions - * - * fan_watchdog (driver): - * fan watchdog interval in seconds, 0 disables (default), max 120 - */ - -/* sysfs fan pwm1_enable ----------------------------------------------- */ -static ssize_t fan_pwm1_enable_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res, mode; - u8 status; - - res = fan_get_status_safe(&status); - if (res) - return res; - - if (status & TP_EC_FAN_FULLSPEED) { - mode = 0; - } else if (status & TP_EC_FAN_AUTO) { - mode = 2; - } else - mode = 1; - - return sysfs_emit(buf, "%d\n", mode); -} - -static ssize_t fan_pwm1_enable_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long t; - int res, level; - - if (parse_strtoul(buf, 2, &t)) - return -EINVAL; - - tpacpi_disclose_usertask("hwmon pwm1_enable", - "set fan mode to %lu\n", t); - - switch (t) { - case 0: - level = TP_EC_FAN_FULLSPEED; - break; - case 1: - level = TPACPI_FAN_LAST_LEVEL; - break; - case 2: - level = TP_EC_FAN_AUTO; - break; - case 3: - /* reserved for software-controlled auto mode */ - return -ENOSYS; - default: - return -EINVAL; - } - - res = fan_set_level_safe(level); - if (res == -ENXIO) - return -EINVAL; - else if (res < 0) - return res; - - fan_watchdog_reset(); - - return count; -} - -static DEVICE_ATTR(pwm1_enable, S_IWUSR | S_IRUGO, - fan_pwm1_enable_show, fan_pwm1_enable_store); - -/* sysfs fan pwm1 ------------------------------------------------------ */ -static ssize_t fan_pwm1_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res; - u8 status; - - res = fan_get_status_safe(&status); - if (res) - return res; - - if ((status & - (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) != 0) - status = fan_control_desired_level; - - if (status > 7) - status = 7; - - return sysfs_emit(buf, "%u\n", (status * 255) / 7); -} - -static ssize_t fan_pwm1_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned long s; - int rc; - u8 status, newlevel; - - if (parse_strtoul(buf, 255, &s)) - return -EINVAL; - - tpacpi_disclose_usertask("hwmon pwm1", - "set fan speed to %lu\n", s); - - /* scale down from 0-255 to 0-7 */ - newlevel = (s >> 5) & 0x07; - - if (mutex_lock_killable(&fan_mutex)) - return -ERESTARTSYS; - - rc = fan_get_status(&status); - if (!rc && (status & - (TP_EC_FAN_AUTO | TP_EC_FAN_FULLSPEED)) == 0) { - rc = fan_set_level(newlevel); - if (rc == -ENXIO) - rc = -EINVAL; - else if (!rc) { - fan_update_desired_level(newlevel); - fan_watchdog_reset(); - } - } - - mutex_unlock(&fan_mutex); - return (rc) ? rc : count; -} - -static DEVICE_ATTR(pwm1, S_IWUSR | S_IRUGO, fan_pwm1_show, fan_pwm1_store); - -/* sysfs fan fan1_input ------------------------------------------------ */ -static ssize_t fan_fan1_input_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res; - unsigned int speed; - - res = fan_get_speed(&speed); - if (res < 0) - return res; - - /* Check for fan speeds displayed in hexadecimal */ - if (!ecfw_with_fan_dec_rpm) - return sysfs_emit(buf, "%u\n", speed); - else - return sysfs_emit(buf, "%x\n", speed); -} - -static DEVICE_ATTR(fan1_input, S_IRUGO, fan_fan1_input_show, NULL); - -/* sysfs fan fan2_input ------------------------------------------------ */ -static ssize_t fan_fan2_input_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int res; - unsigned int speed; - - res = fan2_get_speed(&speed); - if (res < 0) - return res; - - /* Check for fan speeds displayed in hexadecimal */ - if (!ecfw_with_fan_dec_rpm) - return sysfs_emit(buf, "%u\n", speed); - else - return sysfs_emit(buf, "%x\n", speed); -} - -static DEVICE_ATTR(fan2_input, S_IRUGO, fan_fan2_input_show, NULL); - -/* sysfs fan fan_watchdog (hwmon driver) ------------------------------- */ -static ssize_t fan_watchdog_show(struct device_driver *drv, char *buf) -{ - return sysfs_emit(buf, "%u\n", fan_watchdog_maxinterval); -} - -static ssize_t fan_watchdog_store(struct device_driver *drv, const char *buf, - size_t count) -{ - unsigned long t; - - if (parse_strtoul(buf, 120, &t)) - return -EINVAL; - - if (!fan_control_allowed) - return -EPERM; - - fan_watchdog_maxinterval = t; - fan_watchdog_reset(); - - tpacpi_disclose_usertask("fan_watchdog", "set to %lu\n", t); - - return count; -} -static DRIVER_ATTR_RW(fan_watchdog); - -/* --------------------------------------------------------------------- */ - -static struct attribute *fan_attributes[] = { - &dev_attr_pwm1_enable.attr, - &dev_attr_pwm1.attr, - &dev_attr_fan1_input.attr, - &dev_attr_fan2_input.attr, - NULL -}; - -static umode_t fan_attr_is_visible(struct kobject *kobj, struct attribute *attr, - int n) -{ - if (fan_status_access_mode == TPACPI_FAN_NONE && - fan_control_access_mode == TPACPI_FAN_WR_NONE) - return 0; - - if (attr == &dev_attr_fan2_input.attr) { - if (!tp_features.second_fan) - return 0; - } - - return attr->mode; -} - -static const struct attribute_group fan_attr_group = { - .is_visible = fan_attr_is_visible, - .attrs = fan_attributes, -}; - -static struct attribute *fan_driver_attributes[] = { - &driver_attr_fan_watchdog.attr, - NULL -}; - -static const struct attribute_group fan_driver_attr_group = { - .is_visible = fan_attr_is_visible, - .attrs = fan_driver_attributes, -}; - -#define TPACPI_FAN_Q1 0x0001 /* Uninitialized HFSP */ -#define TPACPI_FAN_2FAN 0x0002 /* EC 0x31 bit 0 selects fan2 */ -#define TPACPI_FAN_2CTL 0x0004 /* selects fan2 control */ -#define TPACPI_FAN_NOFAN 0x0008 /* no fan available */ -#define TPACPI_FAN_NS 0x0010 /* For EC with non-Standard register addresses */ -#define TPACPI_FAN_DECRPM 0x0020 /* For ECFW's with RPM in register as decimal */ -#define TPACPI_FAN_TPR 0x0040 /* Fan speed is in Ticks Per Revolution */ -#define TPACPI_FAN_NOACPI 0x0080 /* Don't use ACPI methods even if detected */ - -static const struct tpacpi_quirk fan_quirk_table[] __initconst = { - TPACPI_QEC_IBM('1', 'Y', TPACPI_FAN_Q1), - TPACPI_QEC_IBM('7', '8', TPACPI_FAN_Q1), - TPACPI_QEC_IBM('7', '6', TPACPI_FAN_Q1), - TPACPI_QEC_IBM('7', '0', TPACPI_FAN_Q1), - TPACPI_QEC_LNV('7', 'M', TPACPI_FAN_2FAN), - TPACPI_Q_LNV('N', '1', TPACPI_FAN_2FAN), - TPACPI_Q_LNV3('N', '1', 'D', TPACPI_FAN_2CTL), /* P70 */ - TPACPI_Q_LNV3('N', '1', 'E', TPACPI_FAN_2CTL), /* P50 */ - TPACPI_Q_LNV3('N', '1', 'T', TPACPI_FAN_2CTL), /* P71 */ - TPACPI_Q_LNV3('N', '1', 'U', TPACPI_FAN_2CTL), /* P51 */ - TPACPI_Q_LNV3('N', '2', 'C', TPACPI_FAN_2CTL), /* P52 / P72 */ - TPACPI_Q_LNV3('N', '2', 'N', TPACPI_FAN_2CTL), /* P53 / P73 */ - TPACPI_Q_LNV3('N', '2', 'E', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (1st gen) */ - TPACPI_Q_LNV3('N', '2', 'O', TPACPI_FAN_2CTL), /* P1 / X1 Extreme (2nd gen) */ - TPACPI_Q_LNV3('N', '3', '0', TPACPI_FAN_2CTL), /* P15 (1st gen) / P15v (1st gen) */ - TPACPI_Q_LNV3('N', '3', '7', TPACPI_FAN_2CTL), /* T15g (2nd gen) */ - TPACPI_Q_LNV3('R', '1', 'F', TPACPI_FAN_NS), /* L13 Yoga Gen 2 */ - TPACPI_Q_LNV3('N', '2', 'U', TPACPI_FAN_NS), /* X13 Yoga Gen 2*/ - TPACPI_Q_LNV3('R', '0', 'R', TPACPI_FAN_NS), /* L380 */ - TPACPI_Q_LNV3('R', '1', '5', TPACPI_FAN_NS), /* L13 Yoga Gen 1 */ - TPACPI_Q_LNV3('R', '1', '0', TPACPI_FAN_NS), /* L390 */ - TPACPI_Q_LNV3('N', '2', 'L', TPACPI_FAN_NS), /* X13 Yoga Gen 1 */ - TPACPI_Q_LNV3('R', '0', 'T', TPACPI_FAN_NS), /* 11e Gen5 GL */ - TPACPI_Q_LNV3('R', '1', 'D', TPACPI_FAN_NS), /* 11e Gen5 GL-R */ - TPACPI_Q_LNV3('R', '0', 'V', TPACPI_FAN_NS), /* 11e Gen5 KL-Y */ - TPACPI_Q_LNV3('N', '1', 'O', TPACPI_FAN_NOFAN), /* X1 Tablet (2nd gen) */ - TPACPI_Q_LNV3('R', '0', 'Q', TPACPI_FAN_DECRPM),/* L480 */ - TPACPI_Q_LNV('8', 'F', TPACPI_FAN_TPR), /* ThinkPad x120e */ - TPACPI_Q_LNV3('R', '0', '0', TPACPI_FAN_NOACPI),/* E560 */ - TPACPI_Q_LNV3('R', '1', '2', TPACPI_FAN_NOACPI),/* T495 */ - TPACPI_Q_LNV3('R', '1', '3', TPACPI_FAN_NOACPI),/* T495s */ -}; - -static int __init fan_init(struct ibm_init_struct *iibm) -{ - unsigned long quirks; - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, - "initializing fan subdriver\n"); - - mutex_init(&fan_mutex); - fan_status_access_mode = TPACPI_FAN_NONE; - fan_control_access_mode = TPACPI_FAN_WR_NONE; - fan_control_commands = 0; - fan_watchdog_maxinterval = 0; - tp_features.fan_ctrl_status_undef = 0; - tp_features.second_fan = 0; - tp_features.second_fan_ctl = 0; - fan_control_desired_level = 7; - - if (tpacpi_is_ibm()) { - TPACPI_ACPIHANDLE_INIT(fans); - TPACPI_ACPIHANDLE_INIT(gfan); - TPACPI_ACPIHANDLE_INIT(sfan); - } - if (tpacpi_is_lenovo()) { - TPACPI_ACPIHANDLE_INIT(fang); - TPACPI_ACPIHANDLE_INIT(fanw); - } - - quirks = tpacpi_check_quirks(fan_quirk_table, - ARRAY_SIZE(fan_quirk_table)); - - if (quirks & TPACPI_FAN_NOFAN) { - pr_info("No integrated ThinkPad fan available\n"); - return -ENODEV; - } - - if (quirks & TPACPI_FAN_NS) { - pr_info("ECFW with non-standard fan reg control found\n"); - fan_with_ns_addr = 1; - /* Fan ctrl support from host is undefined for now */ - tp_features.fan_ctrl_status_undef = 1; - } - - /* Check for the EC/BIOS with RPM reported in decimal*/ - if (quirks & TPACPI_FAN_DECRPM) { - pr_info("ECFW with fan RPM as decimal in EC register\n"); - ecfw_with_fan_dec_rpm = 1; - tp_features.fan_ctrl_status_undef = 1; - } - - if (quirks & TPACPI_FAN_NOACPI) { - /* E560, T495, T495s */ - pr_info("Ignoring buggy ACPI fan access method\n"); - fang_handle = NULL; - fanw_handle = NULL; - } - - if (gfan_handle) { - /* 570, 600e/x, 770e, 770x */ - fan_status_access_mode = TPACPI_FAN_RD_ACPI_GFAN; - } else if (fang_handle) { - /* E531 */ - fan_status_access_mode = TPACPI_FAN_RD_ACPI_FANG; - } else { - /* all other ThinkPads: note that even old-style - * ThinkPad ECs supports the fan control register */ - if (fan_with_ns_addr || - likely(acpi_ec_read(fan_status_offset, &fan_control_initial_status))) { - int res; - unsigned int speed; - - fan_status_access_mode = fan_with_ns_addr ? - TPACPI_FAN_RD_TPEC_NS : TPACPI_FAN_RD_TPEC; - - if (quirks & TPACPI_FAN_Q1) - fan_quirk1_setup(); - if (quirks & TPACPI_FAN_TPR) - fan_speed_in_tpr = true; - /* Try and probe the 2nd fan */ - tp_features.second_fan = 1; /* needed for get_speed to work */ - res = fan2_get_speed(&speed); - if (res >= 0 && speed != FAN_NOT_PRESENT) { - /* It responded - so let's assume it's there */ - tp_features.second_fan = 1; - /* fan control not currently available for ns ECFW */ - tp_features.second_fan_ctl = !fan_with_ns_addr; - pr_info("secondary fan control detected & enabled\n"); - } else { - /* Fan not auto-detected */ - tp_features.second_fan = 0; - if (quirks & TPACPI_FAN_2FAN) { - tp_features.second_fan = 1; - pr_info("secondary fan support enabled\n"); - } - if (quirks & TPACPI_FAN_2CTL) { - tp_features.second_fan = 1; - tp_features.second_fan_ctl = 1; - pr_info("secondary fan control enabled\n"); - } - } - } else { - pr_err("ThinkPad ACPI EC access misbehaving, fan status and control unavailable\n"); - return -ENODEV; - } - } - - if (sfan_handle) { - /* 570, 770x-JL */ - fan_control_access_mode = TPACPI_FAN_WR_ACPI_SFAN; - fan_control_commands |= - TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_ENABLE; - } else if (fanw_handle) { - /* E531 */ - fan_control_access_mode = TPACPI_FAN_WR_ACPI_FANW; - fan_control_commands |= - TPACPI_FAN_CMD_LEVEL | TPACPI_FAN_CMD_SPEED | TPACPI_FAN_CMD_ENABLE; - } else { - if (!gfan_handle) { - /* gfan without sfan means no fan control */ - /* all other models implement TP EC 0x2f control */ - - if (fans_handle) { - /* X31, X40, X41 */ - fan_control_access_mode = - TPACPI_FAN_WR_ACPI_FANS; - fan_control_commands |= - TPACPI_FAN_CMD_SPEED | - TPACPI_FAN_CMD_LEVEL | - TPACPI_FAN_CMD_ENABLE; - } else { - fan_control_access_mode = TPACPI_FAN_WR_TPEC; - fan_control_commands |= - TPACPI_FAN_CMD_LEVEL | - TPACPI_FAN_CMD_ENABLE; - } - } - } - - vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, - "fan is %s, modes %d, %d\n", - str_supported(fan_status_access_mode != TPACPI_FAN_NONE || - fan_control_access_mode != TPACPI_FAN_WR_NONE), - fan_status_access_mode, fan_control_access_mode); - - /* fan control master switch */ - if (!fan_control_allowed) { - fan_control_access_mode = TPACPI_FAN_WR_NONE; - fan_control_commands = 0; - dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_FAN, - "fan control features disabled by parameter\n"); - } - - /* update fan_control_desired_level */ - if (fan_status_access_mode != TPACPI_FAN_NONE) - fan_get_status_safe(NULL); - - if (fan_status_access_mode == TPACPI_FAN_NONE && - fan_control_access_mode == TPACPI_FAN_WR_NONE) - return -ENODEV; - - return 0; -} - -static void fan_exit(void) -{ - vdbg_printk(TPACPI_DBG_EXIT | TPACPI_DBG_FAN, - "cancelling any pending fan watchdog tasks\n"); - - cancel_delayed_work(&fan_watchdog_task); - flush_workqueue(tpacpi_wq); -} - -static void fan_suspend(void) -{ - int rc; - - if (!fan_control_allowed) - return; - - /* Store fan status in cache */ - fan_control_resume_level = 0; - rc = fan_get_status_safe(&fan_control_resume_level); - if (rc) - pr_notice("failed to read fan level for later restore during resume: %d\n", - rc); - - /* if it is undefined, don't attempt to restore it. - * KEEP THIS LAST */ - if (tp_features.fan_ctrl_status_undef) - fan_control_resume_level = 0; -} - -static void fan_resume(void) -{ - u8 current_level = 7; - bool do_set = false; - int rc; - - /* DSDT *always* updates status on resume */ - tp_features.fan_ctrl_status_undef = 0; - - if (!fan_control_allowed || - !fan_control_resume_level || - fan_get_status_safe(¤t_level)) - return; - - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_SFAN: - /* never decrease fan level */ - do_set = (fan_control_resume_level > current_level); - break; - case TPACPI_FAN_WR_ACPI_FANS: - case TPACPI_FAN_WR_TPEC: - /* never decrease fan level, scale is: - * TP_EC_FAN_FULLSPEED > 7 >= TP_EC_FAN_AUTO - * - * We expect the firmware to set either 7 or AUTO, but we - * handle FULLSPEED out of paranoia. - * - * So, we can safely only restore FULLSPEED or 7, anything - * else could slow the fan. Restoring AUTO is useless, at - * best that's exactly what the DSDT already set (it is the - * slower it uses). - * - * Always keep in mind that the DSDT *will* have set the - * fans to what the vendor supposes is the best level. We - * muck with it only to speed the fan up. - */ - if (fan_control_resume_level != 7 && - !(fan_control_resume_level & TP_EC_FAN_FULLSPEED)) - return; - else - do_set = !(current_level & TP_EC_FAN_FULLSPEED) && - (current_level != fan_control_resume_level); - break; - default: - return; - } - if (do_set) { - pr_notice("restoring fan level to 0x%02x\n", - fan_control_resume_level); - rc = fan_set_level_safe(fan_control_resume_level); - if (rc < 0) - pr_notice("failed to restore fan level: %d\n", rc); - } -} - -static int fan_read(struct seq_file *m) -{ - int rc; - u8 status; - unsigned int speed = 0; - - switch (fan_status_access_mode) { - case TPACPI_FAN_RD_ACPI_GFAN: - /* 570, 600e/x, 770e, 770x */ - rc = fan_get_status_safe(&status); - if (rc) - return rc; - - seq_printf(m, "status:\t\t%s\n" - "level:\t\t%d\n", - str_enabled_disabled(status), status); - break; - - case TPACPI_FAN_RD_TPEC_NS: - case TPACPI_FAN_RD_TPEC: - case TPACPI_FAN_RD_ACPI_FANG: - /* all except 570, 600e/x, 770e, 770x */ - rc = fan_get_status_safe(&status); - if (rc) - return rc; - - seq_printf(m, "status:\t\t%s\n", str_enabled_disabled(status)); - - rc = fan_get_speed(&speed); - if (rc < 0) - return rc; - - /* Check for fan speeds displayed in hexadecimal */ - if (!ecfw_with_fan_dec_rpm) - seq_printf(m, "speed:\t\t%d\n", speed); - else - seq_printf(m, "speed:\t\t%x\n", speed); - - if (fan_status_access_mode == TPACPI_FAN_RD_TPEC_NS) { - /* - * No full speed bit in NS EC - * EC Auto mode is set by default. - * No other levels settings available - */ - seq_printf(m, "level:\t\t%s\n", status & FAN_NS_CTRL ? "unknown" : "auto"); - } else if (fan_status_access_mode == TPACPI_FAN_RD_TPEC) { - if (status & TP_EC_FAN_FULLSPEED) - /* Disengaged mode takes precedence */ - seq_printf(m, "level:\t\tdisengaged\n"); - else if (status & TP_EC_FAN_AUTO) - seq_printf(m, "level:\t\tauto\n"); - else - seq_printf(m, "level:\t\t%d\n", status); - } - break; - - case TPACPI_FAN_NONE: - default: - seq_printf(m, "status:\t\tnot supported\n"); - } - - if (fan_control_commands & TPACPI_FAN_CMD_LEVEL) { - seq_printf(m, "commands:\tlevel "); - - switch (fan_control_access_mode) { - case TPACPI_FAN_WR_ACPI_SFAN: - seq_printf(m, " ( is 0-7)\n"); - break; - - default: - seq_printf(m, " ( is 0-7, auto, disengaged, full-speed)\n"); - break; - } - } - - if (fan_control_commands & TPACPI_FAN_CMD_ENABLE) - seq_printf(m, "commands:\tenable, disable\n" - "commands:\twatchdog ( is 0 (off), 1-120 (seconds))\n"); - - if (fan_control_commands & TPACPI_FAN_CMD_SPEED) - seq_printf(m, "commands:\tspeed ( is 0-65535)\n"); - - return 0; -} - -static int fan_write_cmd_level(const char *cmd, int *rc) -{ - int level; - - if (strstarts(cmd, "level auto")) - level = TP_EC_FAN_AUTO; - else if (strstarts(cmd, "level disengaged") || strstarts(cmd, "level full-speed")) - level = TP_EC_FAN_FULLSPEED; - else if (sscanf(cmd, "level %d", &level) != 1) - return 0; - - *rc = fan_set_level_safe(level); - if (*rc == -ENXIO) - pr_err("level command accepted for unsupported access mode %d\n", - fan_control_access_mode); - else if (!*rc) - tpacpi_disclose_usertask("procfs fan", - "set level to %d\n", level); - - return 1; -} - -static int fan_write_cmd_enable(const char *cmd, int *rc) -{ - if (!strstarts(cmd, "enable")) - return 0; - - *rc = fan_set_enable(); - if (*rc == -ENXIO) - pr_err("enable command accepted for unsupported access mode %d\n", - fan_control_access_mode); - else if (!*rc) - tpacpi_disclose_usertask("procfs fan", "enable\n"); - - return 1; -} - -static int fan_write_cmd_disable(const char *cmd, int *rc) -{ - if (!strstarts(cmd, "disable")) - return 0; - - *rc = fan_set_disable(); - if (*rc == -ENXIO) - pr_err("disable command accepted for unsupported access mode %d\n", - fan_control_access_mode); - else if (!*rc) - tpacpi_disclose_usertask("procfs fan", "disable\n"); - - return 1; -} - -static int fan_write_cmd_speed(const char *cmd, int *rc) -{ - int speed; - - /* TODO: - * Support speed ? */ - - if (sscanf(cmd, "speed %d", &speed) != 1) - return 0; - - *rc = fan_set_speed(speed); - if (*rc == -ENXIO) - pr_err("speed command accepted for unsupported access mode %d\n", - fan_control_access_mode); - else if (!*rc) - tpacpi_disclose_usertask("procfs fan", - "set speed to %d\n", speed); - - return 1; -} - -static int fan_write_cmd_watchdog(const char *cmd, int *rc) -{ - int interval; - - if (sscanf(cmd, "watchdog %d", &interval) != 1) - return 0; - - if (interval < 0 || interval > 120) - *rc = -EINVAL; - else { - fan_watchdog_maxinterval = interval; - tpacpi_disclose_usertask("procfs fan", - "set watchdog timer to %d\n", - interval); - } - - return 1; -} - -static int fan_write(char *buf) -{ - char *cmd; - int rc = 0; - - while (!rc && (cmd = strsep(&buf, ","))) { - if (!((fan_control_commands & TPACPI_FAN_CMD_LEVEL) && - fan_write_cmd_level(cmd, &rc)) && - !((fan_control_commands & TPACPI_FAN_CMD_ENABLE) && - (fan_write_cmd_enable(cmd, &rc) || - fan_write_cmd_disable(cmd, &rc) || - fan_write_cmd_watchdog(cmd, &rc))) && - !((fan_control_commands & TPACPI_FAN_CMD_SPEED) && - fan_write_cmd_speed(cmd, &rc)) - ) - rc = -EINVAL; - else if (!rc) - fan_watchdog_reset(); - } - - return rc; -} - -static struct ibm_struct fan_driver_data = { - .name = "fan", - .read = fan_read, - .write = fan_write, - .exit = fan_exit, - .suspend = fan_suspend, - .resume = fan_resume, -}; - -/************************************************************************* - * Mute LED subdriver - */ - -#define TPACPI_LED_MAX 2 - -struct tp_led_table { - acpi_string name; - int on_value; - int off_value; - int state; -}; - -static struct tp_led_table led_tables[TPACPI_LED_MAX] = { - [LED_AUDIO_MUTE] = { - .name = "SSMS", - .on_value = 1, - .off_value = 0, - }, - [LED_AUDIO_MICMUTE] = { - .name = "MMTS", - .on_value = 2, - .off_value = 0, - }, -}; - -static int mute_led_on_off(struct tp_led_table *t, bool state) -{ - acpi_handle temp; - int output; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { - pr_warn("Thinkpad ACPI has no %s interface.\n", t->name); - return -EIO; - } - - if (!acpi_evalf(hkey_handle, &output, t->name, "dd", - state ? t->on_value : t->off_value)) - return -EIO; - - t->state = state; - return state; -} - -static int tpacpi_led_set(int whichled, bool on) -{ - struct tp_led_table *t; - - t = &led_tables[whichled]; - if (t->state < 0 || t->state == on) - return t->state; - return mute_led_on_off(t, on); -} - -static int tpacpi_led_mute_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - return tpacpi_led_set(LED_AUDIO_MUTE, brightness != LED_OFF); -} - -static int tpacpi_led_micmute_set(struct led_classdev *led_cdev, - enum led_brightness brightness) -{ - return tpacpi_led_set(LED_AUDIO_MICMUTE, brightness != LED_OFF); -} - -static struct led_classdev mute_led_cdev[TPACPI_LED_MAX] = { - [LED_AUDIO_MUTE] = { - .name = "platform::mute", - .max_brightness = 1, - .brightness_set_blocking = tpacpi_led_mute_set, - .default_trigger = "audio-mute", - }, - [LED_AUDIO_MICMUTE] = { - .name = "platform::micmute", - .max_brightness = 1, - .brightness_set_blocking = tpacpi_led_micmute_set, - .default_trigger = "audio-micmute", - }, -}; - -static int mute_led_init(struct ibm_init_struct *iibm) -{ - acpi_handle temp; - int i, err; - - for (i = 0; i < TPACPI_LED_MAX; i++) { - struct tp_led_table *t = &led_tables[i]; - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, t->name, &temp))) { - t->state = -ENODEV; - continue; - } - - err = led_classdev_register(&tpacpi_pdev->dev, &mute_led_cdev[i]); - if (err < 0) { - while (i--) - led_classdev_unregister(&mute_led_cdev[i]); - return err; - } - } - return 0; -} - -static void mute_led_exit(void) -{ - int i; - - for (i = 0; i < TPACPI_LED_MAX; i++) { - led_classdev_unregister(&mute_led_cdev[i]); - tpacpi_led_set(i, false); - } -} - -static void mute_led_resume(void) -{ - int i; - - for (i = 0; i < TPACPI_LED_MAX; i++) { - struct tp_led_table *t = &led_tables[i]; - if (t->state >= 0) - mute_led_on_off(t, t->state); - } -} - -static struct ibm_struct mute_led_driver_data = { - .name = "mute_led", - .exit = mute_led_exit, - .resume = mute_led_resume, -}; - -/* - * Battery Wear Control Driver - * Contact: Ognjen Galic - */ - -/* Metadata */ - -#define GET_START "BCTG" -#define SET_START "BCCS" -#define GET_STOP "BCSG" -#define SET_STOP "BCSS" -#define GET_DISCHARGE "BDSG" -#define SET_DISCHARGE "BDSS" -#define GET_INHIBIT "BICG" -#define SET_INHIBIT "BICS" - -enum { - BAT_ANY = 0, - BAT_PRIMARY = 1, - BAT_SECONDARY = 2 -}; - -enum { - /* Error condition bit */ - METHOD_ERR = BIT(31), -}; - -enum { - /* This is used in the get/set helpers */ - THRESHOLD_START, - THRESHOLD_STOP, - FORCE_DISCHARGE, - INHIBIT_CHARGE, -}; - -struct tpacpi_battery_data { - int charge_start; - int start_support; - int charge_stop; - int stop_support; - unsigned int charge_behaviours; -}; - -struct tpacpi_battery_driver_data { - struct tpacpi_battery_data batteries[3]; - int individual_addressing; -}; - -static struct tpacpi_battery_driver_data battery_info; - -/* ACPI helpers/functions/probes */ - -/* - * This evaluates a ACPI method call specific to the battery - * ACPI extension. The specifics are that an error is marked - * in the 32rd bit of the response, so we just check that here. - */ -static acpi_status tpacpi_battery_acpi_eval(char *method, int *ret, int param) -{ - int response; - - if (!acpi_evalf(hkey_handle, &response, method, "dd", param)) { - acpi_handle_err(hkey_handle, "%s: evaluate failed", method); - return AE_ERROR; - } - if (response & METHOD_ERR) { - acpi_handle_err(hkey_handle, - "%s evaluated but flagged as error", method); - return AE_ERROR; - } - *ret = response; - return AE_OK; -} - -static int tpacpi_battery_get(int what, int battery, int *ret) -{ - switch (what) { - case THRESHOLD_START: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, ret, battery)) - return -ENODEV; - - /* The value is in the low 8 bits of the response */ - *ret = *ret & 0xFF; - return 0; - case THRESHOLD_STOP: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, ret, battery)) - return -ENODEV; - /* Value is in lower 8 bits */ - *ret = *ret & 0xFF; - /* - * On the stop value, if we return 0 that - * does not make any sense. 0 means Default, which - * means that charging stops at 100%, so we return - * that. - */ - if (*ret == 0) - *ret = 100; - return 0; - case FORCE_DISCHARGE: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, ret, battery)) - return -ENODEV; - /* The force discharge status is in bit 0 */ - *ret = *ret & 0x01; - return 0; - case INHIBIT_CHARGE: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, ret, battery)) - return -ENODEV; - /* The inhibit charge status is in bit 0 */ - *ret = *ret & 0x01; - return 0; - default: - pr_crit("wrong parameter: %d", what); - return -EINVAL; - } -} - -static int tpacpi_battery_set(int what, int battery, int value) -{ - int param, ret; - /* The first 8 bits are the value of the threshold */ - param = value; - /* The battery ID is in bits 8-9, 2 bits */ - param |= battery << 8; - - switch (what) { - case THRESHOLD_START: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_START, &ret, param)) { - pr_err("failed to set charge threshold on battery %d", - battery); - return -ENODEV; - } - return 0; - case THRESHOLD_STOP: - if ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_STOP, &ret, param)) { - pr_err("failed to set stop threshold: %d", battery); - return -ENODEV; - } - return 0; - case FORCE_DISCHARGE: - /* Force discharge is in bit 0, - * break on AC attach is in bit 1 (won't work on some ThinkPads), - * battery ID is in bits 8-9, 2 bits. - */ - if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_DISCHARGE, &ret, param))) { - pr_err("failed to set force discharge on %d", battery); - return -ENODEV; - } - return 0; - case INHIBIT_CHARGE: - /* When setting inhibit charge, we set a default value of - * always breaking on AC detach and the effective time is set to - * be permanent. - * The battery ID is in bits 4-5, 2 bits, - * the effective time is in bits 8-23, 2 bytes. - * A time of FFFF indicates forever. - */ - param = value; - param |= battery << 4; - param |= 0xFFFF << 8; - if (ACPI_FAILURE(tpacpi_battery_acpi_eval(SET_INHIBIT, &ret, param))) { - pr_err("failed to set inhibit charge on %d", battery); - return -ENODEV; - } - return 0; - default: - pr_crit("wrong parameter: %d", what); - return -EINVAL; - } -} - -static int tpacpi_battery_set_validate(int what, int battery, int value) -{ - int ret, v; - - ret = tpacpi_battery_set(what, battery, value); - if (ret < 0) - return ret; - - ret = tpacpi_battery_get(what, battery, &v); - if (ret < 0) - return ret; - - if (v == value) - return 0; - - msleep(500); - - ret = tpacpi_battery_get(what, battery, &v); - if (ret < 0) - return ret; - - if (v == value) - return 0; - - return -EIO; -} - -static int tpacpi_battery_probe(int battery) -{ - int ret = 0; - - memset(&battery_info.batteries[battery], 0, - sizeof(battery_info.batteries[battery])); - - /* - * 1) Get the current start threshold - * 2) Check for support - * 3) Get the current stop threshold - * 4) Check for support - * 5) Get the current force discharge status - * 6) Check for support - * 7) Get the current inhibit charge status - * 8) Check for support - */ - if (acpi_has_method(hkey_handle, GET_START)) { - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_START, &ret, battery)) { - pr_err("Error probing battery %d\n", battery); - return -ENODEV; - } - /* Individual addressing is in bit 9 */ - if (ret & BIT(9)) - battery_info.individual_addressing = true; - /* Support is marked in bit 8 */ - if (ret & BIT(8)) - battery_info.batteries[battery].start_support = 1; - else - return -ENODEV; - if (tpacpi_battery_get(THRESHOLD_START, battery, - &battery_info.batteries[battery].charge_start)) { - pr_err("Error probing battery %d\n", battery); - return -ENODEV; - } - } - if (acpi_has_method(hkey_handle, GET_STOP)) { - if ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_STOP, &ret, battery)) { - pr_err("Error probing battery stop; %d\n", battery); - return -ENODEV; - } - /* Support is marked in bit 8 */ - if (ret & BIT(8)) - battery_info.batteries[battery].stop_support = 1; - else - return -ENODEV; - if (tpacpi_battery_get(THRESHOLD_STOP, battery, - &battery_info.batteries[battery].charge_stop)) { - pr_err("Error probing battery stop: %d\n", battery); - return -ENODEV; - } - } - if (acpi_has_method(hkey_handle, GET_DISCHARGE)) { - if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_DISCHARGE, &ret, battery))) { - pr_err("Error probing battery discharge; %d\n", battery); - return -ENODEV; - } - /* Support is marked in bit 8 */ - if (ret & BIT(8)) - battery_info.batteries[battery].charge_behaviours |= - BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE); - } - if (acpi_has_method(hkey_handle, GET_INHIBIT)) { - if (ACPI_FAILURE(tpacpi_battery_acpi_eval(GET_INHIBIT, &ret, battery))) { - pr_err("Error probing battery inhibit charge; %d\n", battery); - return -ENODEV; - } - /* Support is marked in bit 5 */ - if (ret & BIT(5)) - battery_info.batteries[battery].charge_behaviours |= - BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE); - } - - battery_info.batteries[battery].charge_behaviours |= - BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO); - - pr_info("battery %d registered (start %d, stop %d, behaviours: 0x%x)\n", - battery, - battery_info.batteries[battery].charge_start, - battery_info.batteries[battery].charge_stop, - battery_info.batteries[battery].charge_behaviours); - - return 0; -} - -/* General helper functions */ - -static int tpacpi_battery_get_id(const char *battery_name) -{ - - if (strcmp(battery_name, "BAT0") == 0 || - tp_features.battery_force_primary) - return BAT_PRIMARY; - if (strcmp(battery_name, "BAT1") == 0) - return BAT_SECONDARY; - /* - * If for some reason the battery is not BAT0 nor is it - * BAT1, we will assume it's the default, first battery, - * AKA primary. - */ - pr_warn("unknown battery %s, assuming primary", battery_name); - return BAT_PRIMARY; -} - -/* sysfs interface */ - -static ssize_t tpacpi_battery_store(int what, - struct device *dev, - const char *buf, size_t count) -{ - struct power_supply *supply = to_power_supply(dev); - unsigned long value; - int battery, rval; - /* - * Some systems have support for more than - * one battery. If that is the case, - * tpacpi_battery_probe marked that addressing - * them individually is supported, so we do that - * based on the device struct. - * - * On systems that are not supported, we assume - * the primary as most of the ACPI calls fail - * with "Any Battery" as the parameter. - */ - if (battery_info.individual_addressing) - /* BAT_PRIMARY or BAT_SECONDARY */ - battery = tpacpi_battery_get_id(supply->desc->name); - else - battery = BAT_PRIMARY; - - rval = kstrtoul(buf, 10, &value); - if (rval) - return rval; - - switch (what) { - case THRESHOLD_START: - if (!battery_info.batteries[battery].start_support) - return -ENODEV; - /* valid values are [0, 99] */ - if (value > 99) - return -EINVAL; - if (value > battery_info.batteries[battery].charge_stop) - return -EINVAL; - if (tpacpi_battery_set(THRESHOLD_START, battery, value)) - return -ENODEV; - battery_info.batteries[battery].charge_start = value; - return count; - - case THRESHOLD_STOP: - if (!battery_info.batteries[battery].stop_support) - return -ENODEV; - /* valid values are [1, 100] */ - if (value < 1 || value > 100) - return -EINVAL; - if (value < battery_info.batteries[battery].charge_start) - return -EINVAL; - battery_info.batteries[battery].charge_stop = value; - /* - * When 100 is passed to stop, we need to flip - * it to 0 as that the EC understands that as - * "Default", which will charge to 100% - */ - if (value == 100) - value = 0; - if (tpacpi_battery_set(THRESHOLD_STOP, battery, value)) - return -EINVAL; - return count; - default: - pr_crit("Wrong parameter: %d", what); - return -EINVAL; - } - return count; -} - -static ssize_t tpacpi_battery_show(int what, - struct device *dev, - char *buf) -{ - struct power_supply *supply = to_power_supply(dev); - int ret, battery; - /* - * Some systems have support for more than - * one battery. If that is the case, - * tpacpi_battery_probe marked that addressing - * them individually is supported, so we; - * based on the device struct. - * - * On systems that are not supported, we assume - * the primary as most of the ACPI calls fail - * with "Any Battery" as the parameter. - */ - if (battery_info.individual_addressing) - /* BAT_PRIMARY or BAT_SECONDARY */ - battery = tpacpi_battery_get_id(supply->desc->name); - else - battery = BAT_PRIMARY; - if (tpacpi_battery_get(what, battery, &ret)) - return -ENODEV; - return sysfs_emit(buf, "%d\n", ret); -} - -static ssize_t charge_control_start_threshold_show(struct device *device, - struct device_attribute *attr, - char *buf) -{ - return tpacpi_battery_show(THRESHOLD_START, device, buf); -} - -static ssize_t charge_control_end_threshold_show(struct device *device, - struct device_attribute *attr, - char *buf) -{ - return tpacpi_battery_show(THRESHOLD_STOP, device, buf); -} - -static ssize_t charge_behaviour_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - enum power_supply_charge_behaviour active = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; - struct power_supply *supply = to_power_supply(dev); - unsigned int available; - int ret, battery; - - battery = tpacpi_battery_get_id(supply->desc->name); - available = battery_info.batteries[battery].charge_behaviours; - - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) { - if (tpacpi_battery_get(FORCE_DISCHARGE, battery, &ret)) - return -ENODEV; - if (ret) { - active = POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE; - goto out; - } - } - - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) { - if (tpacpi_battery_get(INHIBIT_CHARGE, battery, &ret)) - return -ENODEV; - if (ret) { - active = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; - goto out; - } - } - -out: - return power_supply_charge_behaviour_show(dev, available, active, buf); -} - -static ssize_t charge_control_start_threshold_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - return tpacpi_battery_store(THRESHOLD_START, dev, buf, count); -} - -static ssize_t charge_control_end_threshold_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - return tpacpi_battery_store(THRESHOLD_STOP, dev, buf, count); -} - -static ssize_t charge_behaviour_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct power_supply *supply = to_power_supply(dev); - int selected, battery, ret = 0; - unsigned int available; - - battery = tpacpi_battery_get_id(supply->desc->name); - available = battery_info.batteries[battery].charge_behaviours; - selected = power_supply_charge_behaviour_parse(available, buf); - - if (selected < 0) - return selected; - - switch (selected) { - case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) - ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) - ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0)); - if (ret < 0) - return ret; - break; - case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)) - ret = tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 0); - ret = min(ret, tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 1)); - if (ret < 0) - return ret; - break; - case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: - if (available & BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) - ret = tpacpi_battery_set_validate(FORCE_DISCHARGE, battery, 0); - ret = min(ret, tpacpi_battery_set_validate(INHIBIT_CHARGE, battery, 1)); - if (ret < 0) - return ret; - break; - default: - dev_err(dev, "Unexpected charge behaviour: %d\n", selected); - return -EINVAL; - } - - return count; -} - -static DEVICE_ATTR_RW(charge_control_start_threshold); -static DEVICE_ATTR_RW(charge_control_end_threshold); -static DEVICE_ATTR_RW(charge_behaviour); -static struct device_attribute dev_attr_charge_start_threshold = __ATTR( - charge_start_threshold, - 0644, - charge_control_start_threshold_show, - charge_control_start_threshold_store -); -static struct device_attribute dev_attr_charge_stop_threshold = __ATTR( - charge_stop_threshold, - 0644, - charge_control_end_threshold_show, - charge_control_end_threshold_store -); - -static struct attribute *tpacpi_battery_attrs[] = { - &dev_attr_charge_control_start_threshold.attr, - &dev_attr_charge_control_end_threshold.attr, - &dev_attr_charge_start_threshold.attr, - &dev_attr_charge_stop_threshold.attr, - &dev_attr_charge_behaviour.attr, - NULL, -}; - -ATTRIBUTE_GROUPS(tpacpi_battery); - -/* ACPI battery hooking */ - -static int tpacpi_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) -{ - int batteryid = tpacpi_battery_get_id(battery->desc->name); - - if (tpacpi_battery_probe(batteryid)) - return -ENODEV; - if (device_add_groups(&battery->dev, tpacpi_battery_groups)) - return -ENODEV; - return 0; -} - -static int tpacpi_battery_remove(struct power_supply *battery, struct acpi_battery_hook *hook) -{ - device_remove_groups(&battery->dev, tpacpi_battery_groups); - return 0; -} - -static struct acpi_battery_hook battery_hook = { - .add_battery = tpacpi_battery_add, - .remove_battery = tpacpi_battery_remove, - .name = "ThinkPad Battery Extension", -}; - -/* Subdriver init/exit */ - -static const struct tpacpi_quirk battery_quirk_table[] __initconst = { - /* - * Individual addressing is broken on models that expose the - * primary battery as BAT1. - */ - TPACPI_Q_LNV('G', '8', true), /* ThinkPad X131e */ - TPACPI_Q_LNV('8', 'F', true), /* Thinkpad X120e */ - TPACPI_Q_LNV('J', '7', true), /* B5400 */ - TPACPI_Q_LNV('J', 'I', true), /* Thinkpad 11e */ - TPACPI_Q_LNV3('R', '0', 'B', true), /* Thinkpad 11e gen 3 */ - TPACPI_Q_LNV3('R', '0', 'C', true), /* Thinkpad 13 */ - TPACPI_Q_LNV3('R', '0', 'J', true), /* Thinkpad 13 gen 2 */ - TPACPI_Q_LNV3('R', '0', 'K', true), /* Thinkpad 11e gen 4 celeron BIOS */ -}; - -static int __init tpacpi_battery_init(struct ibm_init_struct *ibm) -{ - memset(&battery_info, 0, sizeof(battery_info)); - - tp_features.battery_force_primary = tpacpi_check_quirks( - battery_quirk_table, - ARRAY_SIZE(battery_quirk_table)); - - battery_hook_register(&battery_hook); - return 0; -} - -static void tpacpi_battery_exit(void) -{ - battery_hook_unregister(&battery_hook); -} - -static struct ibm_struct battery_driver_data = { - .name = "battery", - .exit = tpacpi_battery_exit, -}; - -/************************************************************************* - * LCD Shadow subdriver, for the Lenovo PrivacyGuard feature - */ - -static struct drm_privacy_screen *lcdshadow_dev; -static acpi_handle lcdshadow_get_handle; -static acpi_handle lcdshadow_set_handle; - -static int lcdshadow_set_sw_state(struct drm_privacy_screen *priv, - enum drm_privacy_screen_status state) -{ - int output; - - if (WARN_ON(!mutex_is_locked(&priv->lock))) - return -EIO; - - if (!acpi_evalf(lcdshadow_set_handle, &output, NULL, "dd", (int)state)) - return -EIO; - - priv->hw_state = priv->sw_state = state; - return 0; -} - -static void lcdshadow_get_hw_state(struct drm_privacy_screen *priv) -{ - int output; - - if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) - return; - - priv->hw_state = priv->sw_state = output & 0x1; -} - -static const struct drm_privacy_screen_ops lcdshadow_ops = { - .set_sw_state = lcdshadow_set_sw_state, - .get_hw_state = lcdshadow_get_hw_state, -}; - -static int tpacpi_lcdshadow_init(struct ibm_init_struct *iibm) -{ - acpi_status status1, status2; - int output; - - status1 = acpi_get_handle(hkey_handle, "GSSS", &lcdshadow_get_handle); - status2 = acpi_get_handle(hkey_handle, "SSSS", &lcdshadow_set_handle); - if (ACPI_FAILURE(status1) || ACPI_FAILURE(status2)) - return 0; - - if (!acpi_evalf(lcdshadow_get_handle, &output, NULL, "dd", 0)) - return -EIO; - - if (!(output & 0x10000)) - return 0; - - lcdshadow_dev = drm_privacy_screen_register(&tpacpi_pdev->dev, - &lcdshadow_ops, NULL); - if (IS_ERR(lcdshadow_dev)) - return PTR_ERR(lcdshadow_dev); - - return 0; -} - -static void lcdshadow_exit(void) -{ - drm_privacy_screen_unregister(lcdshadow_dev); -} - -static void lcdshadow_resume(void) -{ - if (!lcdshadow_dev) - return; - - mutex_lock(&lcdshadow_dev->lock); - lcdshadow_set_sw_state(lcdshadow_dev, lcdshadow_dev->sw_state); - mutex_unlock(&lcdshadow_dev->lock); -} - -static int lcdshadow_read(struct seq_file *m) -{ - if (!lcdshadow_dev) { - seq_puts(m, "status:\t\tnot supported\n"); - } else { - seq_printf(m, "status:\t\t%d\n", lcdshadow_dev->hw_state); - seq_puts(m, "commands:\t0, 1\n"); - } - - return 0; -} - -static int lcdshadow_write(char *buf) -{ - char *cmd; - int res, state = -EINVAL; - - if (!lcdshadow_dev) - return -ENODEV; - - while ((cmd = strsep(&buf, ","))) { - res = kstrtoint(cmd, 10, &state); - if (res < 0) - return res; - } - - if (state >= 2 || state < 0) - return -EINVAL; - - mutex_lock(&lcdshadow_dev->lock); - res = lcdshadow_set_sw_state(lcdshadow_dev, state); - mutex_unlock(&lcdshadow_dev->lock); - - drm_privacy_screen_call_notifier_chain(lcdshadow_dev); - - return res; -} - -static struct ibm_struct lcdshadow_driver_data = { - .name = "lcdshadow", - .exit = lcdshadow_exit, - .resume = lcdshadow_resume, - .read = lcdshadow_read, - .write = lcdshadow_write, -}; - -/************************************************************************* - * Thinkpad sensor interfaces - */ - -#define DYTC_CMD_QUERY 0 /* To get DYTC status - enable/revision */ -#define DYTC_QUERY_ENABLE_BIT 8 /* Bit 8 - 0 = disabled, 1 = enabled */ -#define DYTC_QUERY_SUBREV_BIT 16 /* Bits 16 - 27 - sub revision */ -#define DYTC_QUERY_REV_BIT 28 /* Bits 28 - 31 - revision */ - -#define DYTC_CMD_GET 2 /* To get current IC function and mode */ -#define DYTC_GET_LAPMODE_BIT 17 /* Set when in lapmode */ - -#define PALMSENSOR_PRESENT_BIT 0 /* Determine if psensor present */ -#define PALMSENSOR_ON_BIT 1 /* psensor status */ - -static bool has_palmsensor; -static bool has_lapsensor; -static bool palm_state; -static bool lap_state; -static int dytc_version; - -static int dytc_command(int command, int *output) -{ - acpi_handle dytc_handle; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DYTC", &dytc_handle))) { - /* Platform doesn't support DYTC */ - return -ENODEV; - } - if (!acpi_evalf(dytc_handle, output, NULL, "dd", command)) - return -EIO; - return 0; -} - -static int lapsensor_get(bool *present, bool *state) -{ - int output, err; - - *present = false; - err = dytc_command(DYTC_CMD_GET, &output); - if (err) - return err; - - *present = true; /*If we get his far, we have lapmode support*/ - *state = output & BIT(DYTC_GET_LAPMODE_BIT) ? true : false; - return 0; -} - -static int palmsensor_get(bool *present, bool *state) -{ - acpi_handle psensor_handle; - int output; - - *present = false; - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GPSS", &psensor_handle))) - return -ENODEV; - if (!acpi_evalf(psensor_handle, &output, NULL, "d")) - return -EIO; - - *present = output & BIT(PALMSENSOR_PRESENT_BIT) ? true : false; - *state = output & BIT(PALMSENSOR_ON_BIT) ? true : false; - return 0; -} - -static void lapsensor_refresh(void) -{ - bool state; - int err; - - if (has_lapsensor) { - err = lapsensor_get(&has_lapsensor, &state); - if (err) - return; - if (lap_state != state) { - lap_state = state; - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "dytc_lapmode"); - } - } -} - -static void palmsensor_refresh(void) -{ - bool state; - int err; - - if (has_palmsensor) { - err = palmsensor_get(&has_palmsensor, &state); - if (err) - return; - if (palm_state != state) { - palm_state = state; - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "palmsensor"); - } - } -} - -static ssize_t dytc_lapmode_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - if (has_lapsensor) - return sysfs_emit(buf, "%d\n", lap_state); - return sysfs_emit(buf, "\n"); -} -static DEVICE_ATTR_RO(dytc_lapmode); - -static ssize_t palmsensor_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - if (has_palmsensor) - return sysfs_emit(buf, "%d\n", palm_state); - return sysfs_emit(buf, "\n"); -} -static DEVICE_ATTR_RO(palmsensor); - -static struct attribute *proxsensor_attributes[] = { - &dev_attr_dytc_lapmode.attr, - &dev_attr_palmsensor.attr, - NULL -}; - -static umode_t proxsensor_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - if (attr == &dev_attr_dytc_lapmode.attr) { - /* - * Platforms before DYTC version 5 claim to have a lap sensor, - * but it doesn't work, so we ignore them. - */ - if (!has_lapsensor || dytc_version < 5) - return 0; - } else if (attr == &dev_attr_palmsensor.attr) { - if (!has_palmsensor) - return 0; - } - - return attr->mode; -} - -static const struct attribute_group proxsensor_attr_group = { - .is_visible = proxsensor_attr_is_visible, - .attrs = proxsensor_attributes, -}; - -static int tpacpi_proxsensor_init(struct ibm_init_struct *iibm) -{ - int palm_err, lap_err; - - palm_err = palmsensor_get(&has_palmsensor, &palm_state); - lap_err = lapsensor_get(&has_lapsensor, &lap_state); - /* If support isn't available for both devices return -ENODEV */ - if ((palm_err == -ENODEV) && (lap_err == -ENODEV)) - return -ENODEV; - /* Otherwise, if there was an error return it */ - if (palm_err && (palm_err != -ENODEV)) - return palm_err; - if (lap_err && (lap_err != -ENODEV)) - return lap_err; - - return 0; -} - -static struct ibm_struct proxsensor_driver_data = { - .name = "proximity-sensor", -}; - -/************************************************************************* - * DYTC Platform Profile interface - */ - -#define DYTC_CMD_SET 1 /* To enable/disable IC function mode */ -#define DYTC_CMD_MMC_GET 8 /* To get current MMC function and mode */ -#define DYTC_CMD_RESET 0x1ff /* To reset back to default */ - -#define DYTC_CMD_FUNC_CAP 3 /* To get DYTC capabilities */ -#define DYTC_FC_MMC 27 /* MMC Mode supported */ -#define DYTC_FC_PSC 29 /* PSC Mode supported */ -#define DYTC_FC_AMT 31 /* AMT mode supported */ - -#define DYTC_GET_FUNCTION_BIT 8 /* Bits 8-11 - function setting */ -#define DYTC_GET_MODE_BIT 12 /* Bits 12-15 - mode setting */ - -#define DYTC_SET_FUNCTION_BIT 12 /* Bits 12-15 - function setting */ -#define DYTC_SET_MODE_BIT 16 /* Bits 16-19 - mode setting */ -#define DYTC_SET_VALID_BIT 20 /* Bit 20 - 1 = on, 0 = off */ - -#define DYTC_FUNCTION_STD 0 /* Function = 0, standard mode */ -#define DYTC_FUNCTION_CQL 1 /* Function = 1, lap mode */ -#define DYTC_FUNCTION_MMC 11 /* Function = 11, MMC mode */ -#define DYTC_FUNCTION_PSC 13 /* Function = 13, PSC mode */ -#define DYTC_FUNCTION_AMT 15 /* Function = 15, AMT mode */ - -#define DYTC_MODE_AMT_ENABLE 0x1 /* Enable AMT (in balanced mode) */ -#define DYTC_MODE_AMT_DISABLE 0xF /* Disable AMT (in other modes) */ - -#define DYTC_MODE_MMC_PERFORM 2 /* High power mode aka performance */ -#define DYTC_MODE_MMC_LOWPOWER 3 /* Low power mode */ -#define DYTC_MODE_MMC_BALANCE 0xF /* Default mode aka balanced */ -#define DYTC_MODE_MMC_DEFAULT 0 /* Default mode from MMC_GET, aka balanced */ - -#define DYTC_MODE_PSC_LOWPOWER 3 /* Low power mode */ -#define DYTC_MODE_PSC_BALANCE 5 /* Default mode aka balanced */ -#define DYTC_MODE_PSC_PERFORM 7 /* High power mode aka performance */ - -#define DYTC_MODE_PSCV9_LOWPOWER 1 /* Low power mode */ -#define DYTC_MODE_PSCV9_BALANCE 3 /* Default mode aka balanced */ -#define DYTC_MODE_PSCV9_PERFORM 4 /* High power mode aka performance */ - -#define DYTC_ERR_MASK 0xF /* Bits 0-3 in cmd result are the error result */ -#define DYTC_ERR_SUCCESS 1 /* CMD completed successful */ - -#define DYTC_SET_COMMAND(function, mode, on) \ - (DYTC_CMD_SET | (function) << DYTC_SET_FUNCTION_BIT | \ - (mode) << DYTC_SET_MODE_BIT | \ - (on) << DYTC_SET_VALID_BIT) - -#define DYTC_DISABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 0) -#define DYTC_ENABLE_CQL DYTC_SET_COMMAND(DYTC_FUNCTION_CQL, DYTC_MODE_MMC_BALANCE, 1) -static int dytc_control_amt(bool enable); -static bool dytc_amt_active; - -static enum platform_profile_option dytc_current_profile; -static atomic_t dytc_ignore_event = ATOMIC_INIT(0); -static DEFINE_MUTEX(dytc_mutex); -static int dytc_capabilities; -static bool dytc_mmc_get_available; -static int profile_force; - -static int platform_psc_profile_lowpower = DYTC_MODE_PSC_LOWPOWER; -static int platform_psc_profile_balanced = DYTC_MODE_PSC_BALANCE; -static int platform_psc_profile_performance = DYTC_MODE_PSC_PERFORM; - -static int convert_dytc_to_profile(int funcmode, int dytcmode, - enum platform_profile_option *profile) -{ - switch (funcmode) { - case DYTC_FUNCTION_MMC: - switch (dytcmode) { - case DYTC_MODE_MMC_LOWPOWER: - *profile = PLATFORM_PROFILE_LOW_POWER; - break; - case DYTC_MODE_MMC_DEFAULT: - case DYTC_MODE_MMC_BALANCE: - *profile = PLATFORM_PROFILE_BALANCED; - break; - case DYTC_MODE_MMC_PERFORM: - *profile = PLATFORM_PROFILE_PERFORMANCE; - break; - default: /* Unknown mode */ - return -EINVAL; - } - return 0; - case DYTC_FUNCTION_PSC: - if (dytcmode == platform_psc_profile_lowpower) - *profile = PLATFORM_PROFILE_LOW_POWER; - else if (dytcmode == platform_psc_profile_balanced) - *profile = PLATFORM_PROFILE_BALANCED; - else if (dytcmode == platform_psc_profile_performance) - *profile = PLATFORM_PROFILE_PERFORMANCE; - else - return -EINVAL; - - return 0; - case DYTC_FUNCTION_AMT: - /* For now return balanced. It's the closest we have to 'auto' */ - *profile = PLATFORM_PROFILE_BALANCED; - return 0; - default: - /* Unknown function */ - pr_debug("unknown function 0x%x\n", funcmode); - return -EOPNOTSUPP; - } - return 0; -} - -static int convert_profile_to_dytc(enum platform_profile_option profile, int *perfmode) -{ - switch (profile) { - case PLATFORM_PROFILE_LOW_POWER: - if (dytc_capabilities & BIT(DYTC_FC_MMC)) - *perfmode = DYTC_MODE_MMC_LOWPOWER; - else if (dytc_capabilities & BIT(DYTC_FC_PSC)) - *perfmode = platform_psc_profile_lowpower; - break; - case PLATFORM_PROFILE_BALANCED: - if (dytc_capabilities & BIT(DYTC_FC_MMC)) - *perfmode = DYTC_MODE_MMC_BALANCE; - else if (dytc_capabilities & BIT(DYTC_FC_PSC)) - *perfmode = platform_psc_profile_balanced; - break; - case PLATFORM_PROFILE_PERFORMANCE: - if (dytc_capabilities & BIT(DYTC_FC_MMC)) - *perfmode = DYTC_MODE_MMC_PERFORM; - else if (dytc_capabilities & BIT(DYTC_FC_PSC)) - *perfmode = platform_psc_profile_performance; - break; - default: /* Unknown profile */ - return -EOPNOTSUPP; - } - return 0; -} - -/* - * dytc_profile_get: Function to register with platform_profile - * handler. Returns current platform profile. - */ -static int dytc_profile_get(struct device *dev, - enum platform_profile_option *profile) -{ - *profile = dytc_current_profile; - return 0; -} - -static int dytc_control_amt(bool enable) -{ - int dummy; - int err; - int cmd; - - if (!(dytc_capabilities & BIT(DYTC_FC_AMT))) { - pr_warn("Attempting to toggle AMT on a system that doesn't advertise support\n"); - return -ENODEV; - } - - if (enable) - cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_ENABLE, enable); - else - cmd = DYTC_SET_COMMAND(DYTC_FUNCTION_AMT, DYTC_MODE_AMT_DISABLE, enable); - - pr_debug("%sabling AMT (cmd 0x%x)", enable ? "en":"dis", cmd); - err = dytc_command(cmd, &dummy); - if (err) - return err; - dytc_amt_active = enable; - return 0; -} - -/* - * Helper function - check if we are in CQL mode and if we are - * - disable CQL, - * - run the command - * - enable CQL - * If not in CQL mode, just run the command - */ -static int dytc_cql_command(int command, int *output) -{ - int err, cmd_err, dummy; - int cur_funcmode; - - /* Determine if we are in CQL mode. This alters the commands we do */ - err = dytc_command(DYTC_CMD_GET, output); - if (err) - return err; - - cur_funcmode = (*output >> DYTC_GET_FUNCTION_BIT) & 0xF; - /* Check if we're OK to return immediately */ - if ((command == DYTC_CMD_GET) && (cur_funcmode != DYTC_FUNCTION_CQL)) - return 0; - - if (cur_funcmode == DYTC_FUNCTION_CQL) { - atomic_inc(&dytc_ignore_event); - err = dytc_command(DYTC_DISABLE_CQL, &dummy); - if (err) - return err; - } - - cmd_err = dytc_command(command, output); - /* Check return condition after we've restored CQL state */ - - if (cur_funcmode == DYTC_FUNCTION_CQL) { - err = dytc_command(DYTC_ENABLE_CQL, &dummy); - if (err) - return err; - } - return cmd_err; -} - -/* - * dytc_profile_set: Function to register with platform_profile - * handler. Sets current platform profile. - */ -static int dytc_profile_set(struct device *dev, - enum platform_profile_option profile) -{ - int perfmode; - int output; - int err; - - err = mutex_lock_interruptible(&dytc_mutex); - if (err) - return err; - - err = convert_profile_to_dytc(profile, &perfmode); - if (err) - goto unlock; - - if (dytc_capabilities & BIT(DYTC_FC_MMC)) { - if (profile == PLATFORM_PROFILE_BALANCED) { - /* - * To get back to balanced mode we need to issue a reset command. - * Note we still need to disable CQL mode before hand and re-enable - * it afterwards, otherwise dytc_lapmode gets reset to 0 and stays - * stuck at 0 for aprox. 30 minutes. - */ - err = dytc_cql_command(DYTC_CMD_RESET, &output); - if (err) - goto unlock; - } else { - /* Determine if we are in CQL mode. This alters the commands we do */ - err = dytc_cql_command(DYTC_SET_COMMAND(DYTC_FUNCTION_MMC, perfmode, 1), - &output); - if (err) - goto unlock; - } - } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { - err = dytc_command(DYTC_SET_COMMAND(DYTC_FUNCTION_PSC, perfmode, 1), &output); - if (err) - goto unlock; - - /* system supports AMT, activate it when on balanced */ - if (dytc_capabilities & BIT(DYTC_FC_AMT)) - dytc_control_amt(profile == PLATFORM_PROFILE_BALANCED); - } - /* Success - update current profile */ - dytc_current_profile = profile; -unlock: - mutex_unlock(&dytc_mutex); - return err; -} - -static int dytc_profile_probe(void *drvdata, unsigned long *choices) -{ - set_bit(PLATFORM_PROFILE_LOW_POWER, choices); - set_bit(PLATFORM_PROFILE_BALANCED, choices); - set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); - - return 0; -} - -static const struct platform_profile_ops dytc_profile_ops = { - .probe = dytc_profile_probe, - .profile_get = dytc_profile_get, - .profile_set = dytc_profile_set, -}; - -static void dytc_profile_refresh(void) -{ - enum platform_profile_option profile; - int output = 0, err = 0; - int perfmode, funcmode = 0; - - mutex_lock(&dytc_mutex); - if (dytc_capabilities & BIT(DYTC_FC_MMC)) { - if (dytc_mmc_get_available) - err = dytc_command(DYTC_CMD_MMC_GET, &output); - else - err = dytc_cql_command(DYTC_CMD_GET, &output); - funcmode = DYTC_FUNCTION_MMC; - } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { - err = dytc_command(DYTC_CMD_GET, &output); - /* Check if we are PSC mode, or have AMT enabled */ - funcmode = (output >> DYTC_GET_FUNCTION_BIT) & 0xF; - } else { /* Unknown profile mode */ - err = -ENODEV; - } - mutex_unlock(&dytc_mutex); - if (err) - return; - - perfmode = (output >> DYTC_GET_MODE_BIT) & 0xF; - err = convert_dytc_to_profile(funcmode, perfmode, &profile); - if (!err && profile != dytc_current_profile) { - dytc_current_profile = profile; - platform_profile_notify(tpacpi_pprof); - } -} - -static int tpacpi_dytc_profile_init(struct ibm_init_struct *iibm) -{ - int err, output; - - err = dytc_command(DYTC_CMD_QUERY, &output); - if (err) - return err; - - if (output & BIT(DYTC_QUERY_ENABLE_BIT)) - dytc_version = (output >> DYTC_QUERY_REV_BIT) & 0xF; - - dbg_printk(TPACPI_DBG_INIT, "DYTC version %d\n", dytc_version); - /* Check DYTC is enabled and supports mode setting */ - if (dytc_version < 5) - return -ENODEV; - - /* Check what capabilities are supported */ - err = dytc_command(DYTC_CMD_FUNC_CAP, &dytc_capabilities); - if (err) - return err; - - /* Check if user wants to override the profile selection */ - if (profile_force) { - switch (profile_force) { - case -1: - dytc_capabilities = 0; - break; - case 1: - dytc_capabilities = BIT(DYTC_FC_MMC); - break; - case 2: - dytc_capabilities = BIT(DYTC_FC_PSC); - break; - } - pr_debug("Profile selection forced: 0x%x\n", dytc_capabilities); - } - if (dytc_capabilities & BIT(DYTC_FC_MMC)) { /* MMC MODE */ - pr_debug("MMC is supported\n"); - /* - * Check if MMC_GET functionality available - * Version > 6 and return success from MMC_GET command - */ - dytc_mmc_get_available = false; - if (dytc_version >= 6) { - err = dytc_command(DYTC_CMD_MMC_GET, &output); - if (!err && ((output & DYTC_ERR_MASK) == DYTC_ERR_SUCCESS)) - dytc_mmc_get_available = true; - } - } else if (dytc_capabilities & BIT(DYTC_FC_PSC)) { /* PSC MODE */ - pr_debug("PSC is supported\n"); - if (dytc_version >= 9) { /* update profiles for DYTC 9 and up */ - platform_psc_profile_lowpower = DYTC_MODE_PSCV9_LOWPOWER; - platform_psc_profile_balanced = DYTC_MODE_PSCV9_BALANCE; - platform_psc_profile_performance = DYTC_MODE_PSCV9_PERFORM; - } - } else { - dbg_printk(TPACPI_DBG_INIT, "No DYTC support available\n"); - return -ENODEV; - } - - dbg_printk(TPACPI_DBG_INIT, - "DYTC version %d: thermal mode available\n", dytc_version); - - /* Create platform_profile structure and register */ - tpacpi_pprof = platform_profile_register(&tpacpi_pdev->dev, "thinkpad-acpi-profile", - NULL, &dytc_profile_ops); - /* - * If for some reason platform_profiles aren't enabled - * don't quit terminally. - */ - if (IS_ERR(tpacpi_pprof)) - return -ENODEV; - - /* Ensure initial values are correct */ - dytc_profile_refresh(); - - /* Workaround for https://bugzilla.kernel.org/show_bug.cgi?id=216347 */ - if (dytc_capabilities & BIT(DYTC_FC_PSC)) - dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); - - return 0; -} - -static void dytc_profile_exit(void) -{ - if (!IS_ERR_OR_NULL(tpacpi_pprof)) - platform_profile_remove(tpacpi_pprof); -} - -static struct ibm_struct dytc_profile_driver_data = { - .name = "dytc-profile", - .exit = dytc_profile_exit, -}; - -/************************************************************************* - * Keyboard language interface - */ - -struct keyboard_lang_data { - const char *lang_str; - int lang_code; -}; - -static const struct keyboard_lang_data keyboard_lang_data[] = { - {"be", 0x080c}, - {"cz", 0x0405}, - {"da", 0x0406}, - {"de", 0x0c07}, - {"en", 0x0000}, - {"es", 0x2c0a}, - {"et", 0x0425}, - {"fr", 0x040c}, - {"fr-ch", 0x100c}, - {"hu", 0x040e}, - {"it", 0x0410}, - {"jp", 0x0411}, - {"nl", 0x0413}, - {"nn", 0x0414}, - {"pl", 0x0415}, - {"pt", 0x0816}, - {"sl", 0x041b}, - {"sv", 0x081d}, - {"tr", 0x041f}, -}; - -static int set_keyboard_lang_command(int command) -{ - acpi_handle sskl_handle; - int output; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "SSKL", &sskl_handle))) { - /* Platform doesn't support SSKL */ - return -ENODEV; - } - - if (!acpi_evalf(sskl_handle, &output, NULL, "dd", command)) - return -EIO; - - return 0; -} - -static int get_keyboard_lang(int *output) -{ - acpi_handle gskl_handle; - int kbd_lang; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "GSKL", &gskl_handle))) { - /* Platform doesn't support GSKL */ - return -ENODEV; - } - - if (!acpi_evalf(gskl_handle, &kbd_lang, NULL, "dd", 0x02000000)) - return -EIO; - - /* - * METHOD_ERR gets returned on devices where there are no special (e.g. '=', - * '(' and ')') keys which use layout dependent key-press emulation. - */ - if (kbd_lang & METHOD_ERR) - return -ENODEV; - - *output = kbd_lang; - - return 0; -} - -/* sysfs keyboard language entry */ -static ssize_t keyboard_lang_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int output, err, i, len = 0; - - err = get_keyboard_lang(&output); - if (err) - return err; - - for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { - if (i) - len += sysfs_emit_at(buf, len, "%s", " "); - - if (output == keyboard_lang_data[i].lang_code) { - len += sysfs_emit_at(buf, len, "[%s]", keyboard_lang_data[i].lang_str); - } else { - len += sysfs_emit_at(buf, len, "%s", keyboard_lang_data[i].lang_str); - } - } - len += sysfs_emit_at(buf, len, "\n"); - - return len; -} - -static ssize_t keyboard_lang_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - int err, i; - bool lang_found = false; - int lang_code = 0; - - for (i = 0; i < ARRAY_SIZE(keyboard_lang_data); i++) { - if (sysfs_streq(buf, keyboard_lang_data[i].lang_str)) { - lang_code = keyboard_lang_data[i].lang_code; - lang_found = true; - break; - } - } - - if (lang_found) { - lang_code = lang_code | 1 << 24; - - /* Set language code */ - err = set_keyboard_lang_command(lang_code); - if (err) - return err; - } else { - dev_err(&tpacpi_pdev->dev, "Unknown Keyboard language. Ignoring\n"); - return -EINVAL; - } - - tpacpi_disclose_usertask(attr->attr.name, - "keyboard language is set to %s\n", buf); - - sysfs_notify(&tpacpi_pdev->dev.kobj, NULL, "keyboard_lang"); - - return count; -} -static DEVICE_ATTR_RW(keyboard_lang); - -static struct attribute *kbdlang_attributes[] = { - &dev_attr_keyboard_lang.attr, - NULL -}; - -static umode_t kbdlang_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return tp_features.kbd_lang ? attr->mode : 0; -} - -static const struct attribute_group kbdlang_attr_group = { - .is_visible = kbdlang_attr_is_visible, - .attrs = kbdlang_attributes, -}; - -static int tpacpi_kbdlang_init(struct ibm_init_struct *iibm) -{ - int err, output; - - err = get_keyboard_lang(&output); - tp_features.kbd_lang = !err; - return err; -} - -static struct ibm_struct kbdlang_driver_data = { - .name = "kbdlang", -}; - -/************************************************************************* - * DPRC(Dynamic Power Reduction Control) subdriver, for the Lenovo WWAN - * and WLAN feature. - */ -#define DPRC_GET_WWAN_ANTENNA_TYPE 0x40000 -#define DPRC_WWAN_ANTENNA_TYPE_A_BIT BIT(4) -#define DPRC_WWAN_ANTENNA_TYPE_B_BIT BIT(8) -static bool has_antennatype; -static int wwan_antennatype; - -static int dprc_command(int command, int *output) -{ - acpi_handle dprc_handle; - - if (ACPI_FAILURE(acpi_get_handle(hkey_handle, "DPRC", &dprc_handle))) { - /* Platform doesn't support DPRC */ - return -ENODEV; - } - - if (!acpi_evalf(dprc_handle, output, NULL, "dd", command)) - return -EIO; - - /* - * METHOD_ERR gets returned on devices where few commands are not supported - * for example command to get WWAN Antenna type command is not supported on - * some devices. - */ - if (*output & METHOD_ERR) - return -ENODEV; - - return 0; -} - -static int get_wwan_antenna(int *wwan_antennatype) -{ - int output, err; - - /* Get current Antenna type */ - err = dprc_command(DPRC_GET_WWAN_ANTENNA_TYPE, &output); - if (err) - return err; - - if (output & DPRC_WWAN_ANTENNA_TYPE_A_BIT) - *wwan_antennatype = 1; - else if (output & DPRC_WWAN_ANTENNA_TYPE_B_BIT) - *wwan_antennatype = 2; - else - return -ENODEV; - - return 0; -} - -/* sysfs wwan antenna type entry */ -static ssize_t wwan_antenna_type_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - switch (wwan_antennatype) { - case 1: - return sysfs_emit(buf, "type a\n"); - case 2: - return sysfs_emit(buf, "type b\n"); - default: - return -ENODATA; - } -} -static DEVICE_ATTR_RO(wwan_antenna_type); - -static struct attribute *dprc_attributes[] = { - &dev_attr_wwan_antenna_type.attr, - NULL -}; - -static umode_t dprc_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return has_antennatype ? attr->mode : 0; -} - -static const struct attribute_group dprc_attr_group = { - .is_visible = dprc_attr_is_visible, - .attrs = dprc_attributes, -}; - -static int tpacpi_dprc_init(struct ibm_init_struct *iibm) -{ - int err; - - err = get_wwan_antenna(&wwan_antennatype); - if (err) - return err; - - has_antennatype = true; - return 0; -} - -static struct ibm_struct dprc_driver_data = { - .name = "dprc", -}; - -/* - * Auxmac - * - * This auxiliary mac address is enabled in the bios through the - * MAC Address Pass-through feature. In most cases, there are three - * possibilities: Internal Mac, Second Mac, and disabled. - * - */ - -#define AUXMAC_LEN 12 -#define AUXMAC_START 9 -#define AUXMAC_STRLEN 22 -#define AUXMAC_BEGIN_MARKER 8 -#define AUXMAC_END_MARKER 21 - -static char auxmac[AUXMAC_LEN + 1]; - -static int auxmac_init(struct ibm_init_struct *iibm) -{ - acpi_status status; - struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; - union acpi_object *obj; - - status = acpi_evaluate_object(NULL, "\\MACA", NULL, &buffer); - - if (ACPI_FAILURE(status)) - return -ENODEV; - - obj = buffer.pointer; - - if (obj->type != ACPI_TYPE_STRING || obj->string.length != AUXMAC_STRLEN) { - pr_info("Invalid buffer for MAC address pass-through.\n"); - goto auxmacinvalid; - } - - if (obj->string.pointer[AUXMAC_BEGIN_MARKER] != '#' || - obj->string.pointer[AUXMAC_END_MARKER] != '#') { - pr_info("Invalid header for MAC address pass-through.\n"); - goto auxmacinvalid; - } - - if (strncmp(obj->string.pointer + AUXMAC_START, "XXXXXXXXXXXX", AUXMAC_LEN) != 0) - strscpy(auxmac, obj->string.pointer + AUXMAC_START, sizeof(auxmac)); - else - strscpy(auxmac, "disabled", sizeof(auxmac)); - -free: - kfree(obj); - return 0; - -auxmacinvalid: - strscpy(auxmac, "unavailable", sizeof(auxmac)); - goto free; -} - -static struct ibm_struct auxmac_data = { - .name = "auxmac", -}; - -static DEVICE_STRING_ATTR_RO(auxmac, 0444, auxmac); - -static umode_t auxmac_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - return auxmac[0] == 0 ? 0 : attr->mode; -} - -static struct attribute *auxmac_attributes[] = { - &dev_attr_auxmac.attr.attr, - NULL -}; - -static const struct attribute_group auxmac_attr_group = { - .is_visible = auxmac_attr_is_visible, - .attrs = auxmac_attributes, -}; - -/* --------------------------------------------------------------------- */ - -static struct attribute *tpacpi_driver_attributes[] = { - &driver_attr_debug_level.attr, - &driver_attr_version.attr, - &driver_attr_interface_version.attr, -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - &driver_attr_wlsw_emulstate.attr, - &driver_attr_bluetooth_emulstate.attr, - &driver_attr_wwan_emulstate.attr, - &driver_attr_uwb_emulstate.attr, -#endif - NULL -}; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES -static umode_t tpacpi_attr_is_visible(struct kobject *kobj, - struct attribute *attr, int n) -{ - if (attr == &driver_attr_wlsw_emulstate.attr) { - if (!dbg_wlswemul) - return 0; - } else if (attr == &driver_attr_bluetooth_emulstate.attr) { - if (!dbg_bluetoothemul) - return 0; - } else if (attr == &driver_attr_wwan_emulstate.attr) { - if (!dbg_wwanemul) - return 0; - } else if (attr == &driver_attr_uwb_emulstate.attr) { - if (!dbg_uwbemul) - return 0; - } - - return attr->mode; -} -#endif - -static const struct attribute_group tpacpi_driver_attr_group = { -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES - .is_visible = tpacpi_attr_is_visible, -#endif - .attrs = tpacpi_driver_attributes, -}; - -static const struct attribute_group *tpacpi_driver_groups[] = { - &tpacpi_driver_attr_group, - NULL, -}; - -static const struct attribute_group *tpacpi_groups[] = { - &adaptive_kbd_attr_group, - &hotkey_attr_group, - &bluetooth_attr_group, - &wan_attr_group, - &cmos_attr_group, - &proxsensor_attr_group, - &kbdlang_attr_group, - &dprc_attr_group, - &auxmac_attr_group, - NULL, -}; - -static const struct attribute_group *tpacpi_hwmon_groups[] = { - &thermal_attr_group, - &temp_label_attr_group, - &fan_attr_group, - NULL, -}; - -static const struct attribute_group *tpacpi_hwmon_driver_groups[] = { - &fan_driver_attr_group, - NULL, -}; - -/**************************************************************************** - **************************************************************************** - * - * Platform drivers - * - **************************************************************************** - ****************************************************************************/ - -static struct platform_driver tpacpi_pdriver = { - .driver = { - .name = TPACPI_DRVR_NAME, - .pm = &tpacpi_pm, - .groups = tpacpi_driver_groups, - .dev_groups = tpacpi_groups, - }, - .shutdown = tpacpi_shutdown_handler, -}; - -static struct platform_driver tpacpi_hwmon_pdriver = { - .driver = { - .name = TPACPI_HWMON_DRVR_NAME, - .groups = tpacpi_hwmon_driver_groups, - }, -}; - -/**************************************************************************** - **************************************************************************** - * - * Infrastructure - * - **************************************************************************** - ****************************************************************************/ - -/* - * HKEY event callout for other subdrivers go here - * (yes, it is ugly, but it is quick, safe, and gets the job done - */ -static bool tpacpi_driver_event(const unsigned int hkey_event) -{ - int camera_shutter_state; - - switch (hkey_event) { - case TP_HKEY_EV_BRGHT_UP: - case TP_HKEY_EV_BRGHT_DOWN: - if (ibm_backlight_device) - tpacpi_brightness_notify_change(); - /* - * Key press events are suppressed by default hotkey_user_mask - * and should still be reported if explicitly requested. - */ - return false; - case TP_HKEY_EV_VOL_UP: - case TP_HKEY_EV_VOL_DOWN: - case TP_HKEY_EV_VOL_MUTE: - if (alsa_card) - volume_alsa_notify_change(); - - /* Key events are suppressed by default hotkey_user_mask */ - return false; - case TP_HKEY_EV_KBD_LIGHT: - if (tp_features.kbdlight) { - enum led_brightness brightness; - - mutex_lock(&kbdlight_mutex); - - /* - * Check the brightness actually changed, setting the brightness - * through kbdlight_set_level() also triggers this event. - */ - brightness = kbdlight_sysfs_get(NULL); - if (kbdlight_brightness != brightness) { - kbdlight_brightness = brightness; - led_classdev_notify_brightness_hw_changed( - &tpacpi_led_kbdlight.led_classdev, brightness); - } - - mutex_unlock(&kbdlight_mutex); - } - /* Key events are suppressed by default hotkey_user_mask */ - return false; - case TP_HKEY_EV_DFR_CHANGE_ROW: - adaptive_keyboard_change_row(); - return true; - case TP_HKEY_EV_DFR_S_QUICKVIEW_ROW: - adaptive_keyboard_s_quickview_row(); - return true; - case TP_HKEY_EV_THM_CSM_COMPLETED: - lapsensor_refresh(); - /* If we are already accessing DYTC then skip dytc update */ - if (!atomic_add_unless(&dytc_ignore_event, -1, 0)) - dytc_profile_refresh(); - - return true; - case TP_HKEY_EV_PRIVACYGUARD_TOGGLE: - if (lcdshadow_dev) { - enum drm_privacy_screen_status old_hw_state; - bool changed; - - mutex_lock(&lcdshadow_dev->lock); - old_hw_state = lcdshadow_dev->hw_state; - lcdshadow_get_hw_state(lcdshadow_dev); - changed = lcdshadow_dev->hw_state != old_hw_state; - mutex_unlock(&lcdshadow_dev->lock); - - if (changed) - drm_privacy_screen_call_notifier_chain(lcdshadow_dev); - } - return true; - case TP_HKEY_EV_AMT_TOGGLE: - /* If we're enabling AMT we need to force balanced mode */ - if (!dytc_amt_active) - /* This will also set AMT mode enabled */ - dytc_profile_set(NULL, PLATFORM_PROFILE_BALANCED); - else - dytc_control_amt(!dytc_amt_active); - - return true; - case TP_HKEY_EV_CAMERASHUTTER_TOGGLE: - camera_shutter_state = get_camera_shutter(); - if (camera_shutter_state < 0) { - pr_err("Error retrieving camera shutter state after shutter event\n"); - return true; - } - mutex_lock(&tpacpi_inputdev_send_mutex); - - input_report_switch(tpacpi_inputdev, SW_CAMERA_LENS_COVER, camera_shutter_state); - input_sync(tpacpi_inputdev); - - mutex_unlock(&tpacpi_inputdev_send_mutex); - return true; - case TP_HKEY_EV_DOUBLETAP_TOGGLE: - tp_features.trackpoint_doubletap = !tp_features.trackpoint_doubletap; - return true; - case TP_HKEY_EV_PROFILE_TOGGLE: - case TP_HKEY_EV_PROFILE_TOGGLE2: - platform_profile_cycle(); - return true; - } - - return false; -} - -/* --------------------------------------------------------------------- */ - -/* /proc support */ -static struct proc_dir_entry *proc_dir; - -/* - * Module and infrastructure proble, init and exit handling - */ - -static bool force_load; - -#ifdef CONFIG_THINKPAD_ACPI_DEBUG -static const char * __init str_supported(int is_supported) -{ - static char text_unsupported[] __initdata = "not supported"; - - return (is_supported) ? &text_unsupported[4] : &text_unsupported[0]; -} -#endif /* CONFIG_THINKPAD_ACPI_DEBUG */ - -static void ibm_exit(struct ibm_struct *ibm) -{ - dbg_printk(TPACPI_DBG_EXIT, "removing %s\n", ibm->name); - - list_del_init(&ibm->all_drivers); - - if (ibm->flags.acpi_notify_installed) { - dbg_printk(TPACPI_DBG_EXIT, - "%s: acpi_remove_notify_handler\n", ibm->name); - BUG_ON(!ibm->acpi); - acpi_remove_notify_handler(*ibm->acpi->handle, - ibm->acpi->type, - dispatch_acpi_notify); - ibm->flags.acpi_notify_installed = 0; - } - - if (ibm->flags.proc_created) { - dbg_printk(TPACPI_DBG_EXIT, - "%s: remove_proc_entry\n", ibm->name); - remove_proc_entry(ibm->name, proc_dir); - ibm->flags.proc_created = 0; - } - - if (ibm->flags.acpi_driver_registered) { - dbg_printk(TPACPI_DBG_EXIT, - "%s: acpi_bus_unregister_driver\n", ibm->name); - BUG_ON(!ibm->acpi); - acpi_bus_unregister_driver(ibm->acpi->driver); - kfree(ibm->acpi->driver); - ibm->acpi->driver = NULL; - ibm->flags.acpi_driver_registered = 0; - } - - if (ibm->flags.init_called && ibm->exit) { - ibm->exit(); - ibm->flags.init_called = 0; - } - - dbg_printk(TPACPI_DBG_INIT, "finished removing %s\n", ibm->name); -} - -static int __init ibm_init(struct ibm_init_struct *iibm) -{ - int ret; - struct ibm_struct *ibm = iibm->data; - struct proc_dir_entry *entry; - - BUG_ON(ibm == NULL); - - INIT_LIST_HEAD(&ibm->all_drivers); - - if (ibm->flags.experimental && !experimental) - return 0; - - dbg_printk(TPACPI_DBG_INIT, - "probing for %s\n", ibm->name); - - if (iibm->init) { - ret = iibm->init(iibm); - if (ret > 0 || ret == -ENODEV) - return 0; /* subdriver functionality not available */ - if (ret) - return ret; - - ibm->flags.init_called = 1; - } - - if (ibm->acpi) { - if (ibm->acpi->hid) { - ret = register_tpacpi_subdriver(ibm); - if (ret) - goto err_out; - } - - if (ibm->acpi->notify) { - ret = setup_acpi_notify(ibm); - if (ret == -ENODEV) { - pr_notice("disabling subdriver %s\n", - ibm->name); - ret = 0; - goto err_out; - } - if (ret < 0) - goto err_out; - } - } - - dbg_printk(TPACPI_DBG_INIT, - "%s installed\n", ibm->name); - - if (ibm->read) { - umode_t mode = iibm->base_procfs_mode; - - if (!mode) - mode = S_IRUGO; - if (ibm->write) - mode |= S_IWUSR; - entry = proc_create_data(ibm->name, mode, proc_dir, - &dispatch_proc_ops, ibm); - if (!entry) { - pr_err("unable to create proc entry %s\n", ibm->name); - ret = -ENODEV; - goto err_out; - } - ibm->flags.proc_created = 1; - } - - list_add_tail(&ibm->all_drivers, &tpacpi_all_drivers); - - return 0; - -err_out: - dbg_printk(TPACPI_DBG_INIT, - "%s: at error exit path with result %d\n", - ibm->name, ret); - - ibm_exit(ibm); - return (ret < 0) ? ret : 0; -} - -/* Probing */ - -static char __init tpacpi_parse_fw_id(const char * const s, - u32 *model, u16 *release) -{ - int i; - - if (!s || strlen(s) < 8) - goto invalid; - - for (i = 0; i < 8; i++) - if (!((s[i] >= '0' && s[i] <= '9') || - (s[i] >= 'A' && s[i] <= 'Z'))) - goto invalid; - - /* - * Most models: xxyTkkWW (#.##c) - * Ancient 570/600 and -SL lacks (#.##c) - */ - if (s[3] == 'T' || s[3] == 'N') { - *model = TPID(s[0], s[1]); - *release = TPVER(s[4], s[5]); - return s[2]; - - /* New models: xxxyTkkW (#.##c); T550 and some others */ - } else if (s[4] == 'T' || s[4] == 'N') { - *model = TPID3(s[0], s[1], s[2]); - *release = TPVER(s[5], s[6]); - return s[3]; - } - -invalid: - return '\0'; -} - -#define EC_FW_STRING_LEN 18 - -static void find_new_ec_fwstr(const struct dmi_header *dm, void *private) -{ - char *ec_fw_string = (char *) private; - const char *dmi_data = (const char *)dm; - /* - * ThinkPad Embedded Controller Program Table on newer models - * - * Offset | Name | Width | Description - * ---------------------------------------------------- - * 0x00 | Type | BYTE | 0x8C - * 0x01 | Length | BYTE | - * 0x02 | Handle | WORD | Varies - * 0x04 | Signature | BYTEx6 | ASCII for "LENOVO" - * 0x0A | OEM struct offset | BYTE | 0x0B - * 0x0B | OEM struct number | BYTE | 0x07, for this structure - * 0x0C | OEM struct revision | BYTE | 0x01, for this format - * 0x0D | ECP version ID | STR ID | - * 0x0E | ECP release date | STR ID | - */ - - /* Return if data structure not match */ - if (dm->type != 140 || dm->length < 0x0F || - memcmp(dmi_data + 4, "LENOVO", 6) != 0 || - dmi_data[0x0A] != 0x0B || dmi_data[0x0B] != 0x07 || - dmi_data[0x0C] != 0x01) - return; - - /* fwstr is the first 8byte string */ - BUILD_BUG_ON(EC_FW_STRING_LEN <= 8); - memcpy(ec_fw_string, dmi_data + 0x0F, 8); -} - -/* returns 0 - probe ok, or < 0 - probe error. - * Probe ok doesn't mean thinkpad found. - * On error, kfree() cleanup on tp->* is not performed, caller must do it */ -static int __must_check __init get_thinkpad_model_data( - struct thinkpad_id_data *tp) -{ - const struct dmi_device *dev = NULL; - char ec_fw_string[EC_FW_STRING_LEN] = {0}; - char const *s; - char t; - - if (!tp) - return -EINVAL; - - memset(tp, 0, sizeof(*tp)); - - if (dmi_name_in_vendors("IBM")) - tp->vendor = PCI_VENDOR_ID_IBM; - else if (dmi_name_in_vendors("LENOVO")) - tp->vendor = PCI_VENDOR_ID_LENOVO; - else if (dmi_name_in_vendors("NEC")) - tp->vendor = PCI_VENDOR_ID_LENOVO; - else - return 0; - - s = dmi_get_system_info(DMI_BIOS_VERSION); - tp->bios_version_str = kstrdup(s, GFP_KERNEL); - if (s && !tp->bios_version_str) - return -ENOMEM; - - /* Really ancient ThinkPad 240X will fail this, which is fine */ - t = tpacpi_parse_fw_id(tp->bios_version_str, - &tp->bios_model, &tp->bios_release); - if (t != 'E' && t != 'C') - return 0; - - /* - * ThinkPad T23 or newer, A31 or newer, R50e or newer, - * X32 or newer, all Z series; Some models must have an - * up-to-date BIOS or they will not be detected. - * - * See https://thinkwiki.org/wiki/List_of_DMI_IDs - */ - while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev))) { - if (sscanf(dev->name, - "IBM ThinkPad Embedded Controller -[%17c", - ec_fw_string) == 1) { - ec_fw_string[sizeof(ec_fw_string) - 1] = 0; - ec_fw_string[strcspn(ec_fw_string, " ]")] = 0; - break; - } - } - - /* Newer ThinkPads have different EC program info table */ - if (!ec_fw_string[0]) - dmi_walk(find_new_ec_fwstr, &ec_fw_string); - - if (ec_fw_string[0]) { - tp->ec_version_str = kstrdup(ec_fw_string, GFP_KERNEL); - if (!tp->ec_version_str) - return -ENOMEM; - - t = tpacpi_parse_fw_id(ec_fw_string, - &tp->ec_model, &tp->ec_release); - if (t != 'H') { - pr_notice("ThinkPad firmware release %s doesn't match the known patterns\n", - ec_fw_string); - pr_notice("please report this to %s\n", TPACPI_MAIL); - } - } - - s = dmi_get_system_info(DMI_PRODUCT_VERSION); - if (s && !(strncasecmp(s, "ThinkPad", 8) && strncasecmp(s, "Lenovo", 6))) { - tp->model_str = kstrdup(s, GFP_KERNEL); - if (!tp->model_str) - return -ENOMEM; - } else { - s = dmi_get_system_info(DMI_BIOS_VENDOR); - if (s && !(strncasecmp(s, "Lenovo", 6))) { - tp->model_str = kstrdup(s, GFP_KERNEL); - if (!tp->model_str) - return -ENOMEM; - } - } - - s = dmi_get_system_info(DMI_PRODUCT_NAME); - tp->nummodel_str = kstrdup(s, GFP_KERNEL); - if (s && !tp->nummodel_str) - return -ENOMEM; - - return 0; -} - -static int __init probe_for_thinkpad(void) -{ - int is_thinkpad; - - if (acpi_disabled) - return -ENODEV; - - /* It would be dangerous to run the driver in this case */ - if (!tpacpi_is_ibm() && !tpacpi_is_lenovo()) - return -ENODEV; - - /* - * Non-ancient models have better DMI tagging, but very old models - * don't. tpacpi_is_fw_known() is a cheat to help in that case. - */ - is_thinkpad = (thinkpad_id.model_str != NULL) || - (thinkpad_id.ec_model != 0) || - tpacpi_is_fw_known(); - - /* The EC handler is required */ - tpacpi_acpi_handle_locate("ec", TPACPI_ACPI_EC_HID, &ec_handle); - if (!ec_handle) { - if (is_thinkpad) - pr_err("Not yet supported ThinkPad detected!\n"); - return -ENODEV; - } - - if (!is_thinkpad && !force_load) - return -ENODEV; - - return 0; -} - -static void __init thinkpad_acpi_init_banner(void) -{ - pr_info("%s v%s\n", TPACPI_DESC, TPACPI_VERSION); - pr_info("%s\n", TPACPI_URL); - - pr_info("ThinkPad BIOS %s, EC %s\n", - (thinkpad_id.bios_version_str) ? - thinkpad_id.bios_version_str : "unknown", - (thinkpad_id.ec_version_str) ? - thinkpad_id.ec_version_str : "unknown"); - - BUG_ON(!thinkpad_id.vendor); - - if (thinkpad_id.model_str) - pr_info("%s %s, model %s\n", - (thinkpad_id.vendor == PCI_VENDOR_ID_IBM) ? - "IBM" : ((thinkpad_id.vendor == - PCI_VENDOR_ID_LENOVO) ? - "Lenovo" : "Unknown vendor"), - thinkpad_id.model_str, - (thinkpad_id.nummodel_str) ? - thinkpad_id.nummodel_str : "unknown"); -} - -/* Module init, exit, parameters */ - -static struct ibm_init_struct ibms_init[] __initdata = { - { - .data = &thinkpad_acpi_driver_data, - }, - { - .init = hotkey_init, - .data = &hotkey_driver_data, - }, - { - .init = bluetooth_init, - .data = &bluetooth_driver_data, - }, - { - .init = wan_init, - .data = &wan_driver_data, - }, - { - .init = uwb_init, - .data = &uwb_driver_data, - }, -#ifdef CONFIG_THINKPAD_ACPI_VIDEO - { - .init = video_init, - .base_procfs_mode = S_IRUSR, - .data = &video_driver_data, - }, -#endif - { - .init = kbdlight_init, - .data = &kbdlight_driver_data, - }, - { - .init = light_init, - .data = &light_driver_data, - }, - { - .init = cmos_init, - .data = &cmos_driver_data, - }, - { - .init = led_init, - .data = &led_driver_data, - }, - { - .init = beep_init, - .data = &beep_driver_data, - }, - { - .init = thermal_init, - .data = &thermal_driver_data, - }, - { - .init = brightness_init, - .data = &brightness_driver_data, - }, - { - .init = volume_init, - .data = &volume_driver_data, - }, - { - .init = fan_init, - .data = &fan_driver_data, - }, - { - .init = mute_led_init, - .data = &mute_led_driver_data, - }, - { - .init = tpacpi_battery_init, - .data = &battery_driver_data, - }, - { - .init = tpacpi_lcdshadow_init, - .data = &lcdshadow_driver_data, - }, - { - .init = tpacpi_proxsensor_init, - .data = &proxsensor_driver_data, - }, - { - .init = tpacpi_dytc_profile_init, - .data = &dytc_profile_driver_data, - }, - { - .init = tpacpi_kbdlang_init, - .data = &kbdlang_driver_data, - }, - { - .init = tpacpi_dprc_init, - .data = &dprc_driver_data, - }, - { - .init = auxmac_init, - .data = &auxmac_data, - }, -}; - -static int __init set_ibm_param(const char *val, const struct kernel_param *kp) -{ - unsigned int i; - struct ibm_struct *ibm; - - if (!kp || !kp->name || !val) - return -EINVAL; - - for (i = 0; i < ARRAY_SIZE(ibms_init); i++) { - ibm = ibms_init[i].data; - if (!ibm || !ibm->name) - continue; - - if (strcmp(ibm->name, kp->name) == 0 && ibm->write) { - if (strlen(val) > sizeof(ibms_init[i].param) - 1) - return -ENOSPC; - strscpy(ibms_init[i].param, val); - return 0; - } - } - - return -EINVAL; -} - -module_param(experimental, int, 0444); -MODULE_PARM_DESC(experimental, - "Enables experimental features when non-zero"); - -module_param_named(debug, dbg_level, uint, 0); -MODULE_PARM_DESC(debug, "Sets debug level bit-mask"); - -module_param(force_load, bool, 0444); -MODULE_PARM_DESC(force_load, - "Attempts to load the driver even on a mis-identified ThinkPad when true"); - -module_param_named(fan_control, fan_control_allowed, bool, 0444); -MODULE_PARM_DESC(fan_control, - "Enables setting fan parameters features when true"); - -module_param_named(brightness_mode, brightness_mode, uint, 0444); -MODULE_PARM_DESC(brightness_mode, - "Selects brightness control strategy: 0=auto, 1=EC, 2=UCMS, 3=EC+NVRAM"); - -module_param(brightness_enable, uint, 0444); -MODULE_PARM_DESC(brightness_enable, - "Enables backlight control when 1, disables when 0"); - -#ifdef CONFIG_THINKPAD_ACPI_ALSA_SUPPORT -module_param_named(volume_mode, volume_mode, uint, 0444); -MODULE_PARM_DESC(volume_mode, - "Selects volume control strategy: 0=auto, 1=EC, 2=N/A, 3=EC+NVRAM"); - -module_param_named(volume_capabilities, volume_capabilities, uint, 0444); -MODULE_PARM_DESC(volume_capabilities, - "Selects the mixer capabilities: 0=auto, 1=volume and mute, 2=mute only"); - -module_param_named(volume_control, volume_control_allowed, bool, 0444); -MODULE_PARM_DESC(volume_control, - "Enables software override for the console audio control when true"); - -module_param_named(software_mute, software_mute_requested, bool, 0444); -MODULE_PARM_DESC(software_mute, - "Request full software mute control"); - -/* ALSA module API parameters */ -module_param_named(index, alsa_index, int, 0444); -MODULE_PARM_DESC(index, "ALSA index for the ACPI EC Mixer"); -module_param_named(id, alsa_id, charp, 0444); -MODULE_PARM_DESC(id, "ALSA id for the ACPI EC Mixer"); -module_param_named(enable, alsa_enable, bool, 0444); -MODULE_PARM_DESC(enable, "Enable the ALSA interface for the ACPI EC Mixer"); -#endif /* CONFIG_THINKPAD_ACPI_ALSA_SUPPORT */ - -/* The module parameter can't be read back, that's why 0 is used here */ -#define TPACPI_PARAM(feature) \ - module_param_call(feature, set_ibm_param, NULL, NULL, 0); \ - MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command at module load, see documentation") - -TPACPI_PARAM(hotkey); -TPACPI_PARAM(bluetooth); -TPACPI_PARAM(video); -TPACPI_PARAM(light); -TPACPI_PARAM(cmos); -TPACPI_PARAM(led); -TPACPI_PARAM(beep); -TPACPI_PARAM(brightness); -TPACPI_PARAM(volume); -TPACPI_PARAM(fan); - -#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES -module_param(dbg_wlswemul, uint, 0444); -MODULE_PARM_DESC(dbg_wlswemul, "Enables WLSW emulation"); -module_param_named(wlsw_state, tpacpi_wlsw_emulstate, bool, 0); -MODULE_PARM_DESC(wlsw_state, - "Initial state of the emulated WLSW switch"); - -module_param(dbg_bluetoothemul, uint, 0444); -MODULE_PARM_DESC(dbg_bluetoothemul, "Enables bluetooth switch emulation"); -module_param_named(bluetooth_state, tpacpi_bluetooth_emulstate, bool, 0); -MODULE_PARM_DESC(bluetooth_state, - "Initial state of the emulated bluetooth switch"); - -module_param(dbg_wwanemul, uint, 0444); -MODULE_PARM_DESC(dbg_wwanemul, "Enables WWAN switch emulation"); -module_param_named(wwan_state, tpacpi_wwan_emulstate, bool, 0); -MODULE_PARM_DESC(wwan_state, - "Initial state of the emulated WWAN switch"); - -module_param(dbg_uwbemul, uint, 0444); -MODULE_PARM_DESC(dbg_uwbemul, "Enables UWB switch emulation"); -module_param_named(uwb_state, tpacpi_uwb_emulstate, bool, 0); -MODULE_PARM_DESC(uwb_state, - "Initial state of the emulated UWB switch"); -#endif - -module_param(profile_force, int, 0444); -MODULE_PARM_DESC(profile_force, "Force profile mode. -1=off, 1=MMC, 2=PSC"); - -static void thinkpad_acpi_module_exit(void) -{ - tpacpi_lifecycle = TPACPI_LIFE_EXITING; - - if (tpacpi_sensors_pdev) { - platform_driver_unregister(&tpacpi_hwmon_pdriver); - platform_device_unregister(tpacpi_sensors_pdev); - } - - if (tp_features.platform_drv_registered) - platform_driver_unregister(&tpacpi_pdriver); - if (tpacpi_pdev) - platform_device_unregister(tpacpi_pdev); - - if (proc_dir) - remove_proc_entry(TPACPI_PROC_DIR, acpi_root_dir); - if (tpacpi_wq) - destroy_workqueue(tpacpi_wq); - - kfree(thinkpad_id.bios_version_str); - kfree(thinkpad_id.ec_version_str); - kfree(thinkpad_id.model_str); - kfree(thinkpad_id.nummodel_str); -} - -static void tpacpi_subdrivers_release(void *data) -{ - struct ibm_struct *ibm, *itmp; - - list_for_each_entry_safe_reverse(ibm, itmp, &tpacpi_all_drivers, all_drivers) - ibm_exit(ibm); - - dbg_printk(TPACPI_DBG_INIT, "finished subdriver exit path...\n"); -} - -static int __init tpacpi_pdriver_probe(struct platform_device *pdev) -{ - int ret; - - ret = devm_mutex_init(&pdev->dev, &tpacpi_inputdev_send_mutex); - if (ret) - return ret; - - tpacpi_inputdev = devm_input_allocate_device(&pdev->dev); - if (!tpacpi_inputdev) - return -ENOMEM; - - tpacpi_inputdev->name = "ThinkPad Extra Buttons"; - tpacpi_inputdev->phys = TPACPI_DRVR_NAME "/input0"; - tpacpi_inputdev->id.bustype = BUS_HOST; - tpacpi_inputdev->id.vendor = thinkpad_id.vendor; - tpacpi_inputdev->id.product = TPACPI_HKEY_INPUT_PRODUCT; - tpacpi_inputdev->id.version = TPACPI_HKEY_INPUT_VERSION; - tpacpi_inputdev->dev.parent = &tpacpi_pdev->dev; - - /* Init subdriver dependencies */ - tpacpi_detect_brightness_capabilities(); - - /* Init subdrivers */ - for (unsigned int i = 0; i < ARRAY_SIZE(ibms_init); i++) { - ret = ibm_init(&ibms_init[i]); - if (ret >= 0 && *ibms_init[i].param) - ret = ibms_init[i].data->write(ibms_init[i].param); - if (ret < 0) { - tpacpi_subdrivers_release(NULL); - return ret; - } - } - - ret = devm_add_action_or_reset(&pdev->dev, tpacpi_subdrivers_release, NULL); - if (ret) - return ret; - - ret = input_register_device(tpacpi_inputdev); - if (ret < 0) - pr_err("unable to register input device\n"); - - return ret; -} - -static int __init tpacpi_hwmon_pdriver_probe(struct platform_device *pdev) -{ - tpacpi_hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, TPACPI_NAME, - NULL, tpacpi_hwmon_groups); - if (IS_ERR(tpacpi_hwmon)) - pr_err("unable to register hwmon device\n"); - - return PTR_ERR_OR_ZERO(tpacpi_hwmon); -} - -static int __init thinkpad_acpi_module_init(void) -{ - const struct dmi_system_id *dmi_id; - int ret; - acpi_object_type obj_type; - - tpacpi_lifecycle = TPACPI_LIFE_INIT; - - /* Driver-level probe */ - - ret = get_thinkpad_model_data(&thinkpad_id); - if (ret) { - pr_err("unable to get DMI data: %d\n", ret); - thinkpad_acpi_module_exit(); - return ret; - } - ret = probe_for_thinkpad(); - if (ret) { - thinkpad_acpi_module_exit(); - return ret; - } - - /* Driver initialization */ - - thinkpad_acpi_init_banner(); - tpacpi_check_outdated_fw(); - - TPACPI_ACPIHANDLE_INIT(ecrd); - TPACPI_ACPIHANDLE_INIT(ecwr); - - /* - * Quirk: in some models (e.g. X380 Yoga), an object named ECRD - * exists, but it is a register, not a method. - */ - if (ecrd_handle) { - acpi_get_type(ecrd_handle, &obj_type); - if (obj_type != ACPI_TYPE_METHOD) - ecrd_handle = NULL; - } - if (ecwr_handle) { - acpi_get_type(ecwr_handle, &obj_type); - if (obj_type != ACPI_TYPE_METHOD) - ecwr_handle = NULL; - } - - tpacpi_wq = create_singlethread_workqueue(TPACPI_WORKQUEUE_NAME); - if (!tpacpi_wq) { - thinkpad_acpi_module_exit(); - return -ENOMEM; - } - - proc_dir = proc_mkdir(TPACPI_PROC_DIR, acpi_root_dir); - if (!proc_dir) { - pr_err("unable to create proc dir " TPACPI_PROC_DIR "\n"); - thinkpad_acpi_module_exit(); - return -ENODEV; - } - - dmi_id = dmi_first_match(fwbug_list); - if (dmi_id) - tp_features.quirks = dmi_id->driver_data; - - /* Device initialization */ - tpacpi_pdev = platform_device_register_simple(TPACPI_DRVR_NAME, PLATFORM_DEVID_NONE, - NULL, 0); - if (IS_ERR(tpacpi_pdev)) { - ret = PTR_ERR(tpacpi_pdev); - tpacpi_pdev = NULL; - pr_err("unable to register platform device\n"); - thinkpad_acpi_module_exit(); - return ret; - } - - ret = platform_driver_probe(&tpacpi_pdriver, tpacpi_pdriver_probe); - if (ret) { - pr_err("unable to register main platform driver\n"); - thinkpad_acpi_module_exit(); - return ret; - } - tp_features.platform_drv_registered = 1; - - tpacpi_sensors_pdev = platform_create_bundle(&tpacpi_hwmon_pdriver, - tpacpi_hwmon_pdriver_probe, - NULL, 0, NULL, 0); - if (IS_ERR(tpacpi_sensors_pdev)) { - ret = PTR_ERR(tpacpi_sensors_pdev); - tpacpi_sensors_pdev = NULL; - pr_err("unable to register hwmon platform device/driver bundle\n"); - thinkpad_acpi_module_exit(); - return ret; - } - - tpacpi_lifecycle = TPACPI_LIFE_RUNNING; - - return 0; -} - -MODULE_ALIAS(TPACPI_DRVR_SHORTNAME); - -/* - * This will autoload the driver in almost every ThinkPad - * in widespread use. - * - * Only _VERY_ old models, like the 240, 240x and 570 lack - * the HKEY event interface. - */ -MODULE_DEVICE_TABLE(acpi, ibm_htk_device_ids); - -/* - * DMI matching for module autoloading - * - * See https://thinkwiki.org/wiki/List_of_DMI_IDs - * See https://thinkwiki.org/wiki/BIOS_Upgrade_Downloads - * - * Only models listed in thinkwiki will be supported, so add yours - * if it is not there yet. - */ -#define IBM_BIOS_MODULE_ALIAS(__type) \ - MODULE_ALIAS("dmi:bvnIBM:bvr" __type "ET??WW*") - -/* Ancient thinkpad BIOSes have to be identified by - * BIOS type or model number, and there are far less - * BIOS types than model numbers... */ -IBM_BIOS_MODULE_ALIAS("I[MU]"); /* 570, 570e */ - -MODULE_AUTHOR("Borislav Deianov "); -MODULE_AUTHOR("Henrique de Moraes Holschuh "); -MODULE_DESCRIPTION(TPACPI_DESC); -MODULE_VERSION(TPACPI_VERSION); -MODULE_LICENSE("GPL"); - -module_init(thinkpad_acpi_module_init); -module_exit(thinkpad_acpi_module_exit); -- cgit v1.2.3 From da8f2708f9b69707f4efeb432a18395e46b4666f Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Wed, 14 May 2025 22:10:52 +0200 Subject: platform/x86: ideapad: Expose charge_types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Ideapad models support a battery conservation mode which limits the battery charge threshold for longer battery longevity. This is currently exposed via a custom conservation_mode attribute in sysfs. The newly introduced charge_types sysfs attribute is a standardized replacement for laptops with a fixed end charge threshold. Setting it to `Long Life` would enable battery conservation mode. The standardized user space API would allow applications such as UPower to detect laptops which support this battery longevity mode and set it. Tested on an Lenovo ideapad U330p. Signed-off-by: Jelle van der Waa Suggested-By: Hans de Goede Reviewed-by: Thomas Weißschuh Reviewed-by: Armin Wolf Link: https://lore.kernel.org/r/20250514201054.381320-1-jvanderwaa@redhat.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- .../ABI/obsolete/sysfs-platform-ideapad-laptop | 8 ++ .../ABI/testing/sysfs-platform-ideapad-laptop | 9 -- drivers/platform/x86/lenovo/Kconfig | 1 + drivers/platform/x86/lenovo/ideapad-laptop.c | 110 ++++++++++++++++++++- 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 Documentation/ABI/obsolete/sysfs-platform-ideapad-laptop (limited to 'drivers') diff --git a/Documentation/ABI/obsolete/sysfs-platform-ideapad-laptop b/Documentation/ABI/obsolete/sysfs-platform-ideapad-laptop new file mode 100644 index 000000000000..c1dbd19c679c --- /dev/null +++ b/Documentation/ABI/obsolete/sysfs-platform-ideapad-laptop @@ -0,0 +1,8 @@ +What: /sys/bus/platform/devices/VPC2004:*/conservation_mode +Date: Aug 2017 +KernelVersion: 4.14 +Contact: platform-driver-x86@vger.kernel.org +Description: + Controls whether the conservation mode is enabled or not. + This feature limits the maximum battery charge percentage to + around 50-60% in order to prolong the lifetime of the battery. diff --git a/Documentation/ABI/testing/sysfs-platform-ideapad-laptop b/Documentation/ABI/testing/sysfs-platform-ideapad-laptop index 4989ab266682..5ec0dee9e707 100644 --- a/Documentation/ABI/testing/sysfs-platform-ideapad-laptop +++ b/Documentation/ABI/testing/sysfs-platform-ideapad-laptop @@ -27,15 +27,6 @@ Description: * 1 -> Switched On * 0 -> Switched Off -What: /sys/bus/platform/devices/VPC2004:*/conservation_mode -Date: Aug 2017 -KernelVersion: 4.14 -Contact: platform-driver-x86@vger.kernel.org -Description: - Controls whether the conservation mode is enabled or not. - This feature limits the maximum battery charge percentage to - around 50-60% in order to prolong the lifetime of the battery. - What: /sys/bus/platform/devices/VPC2004:*/fn_lock Date: May 2018 KernelVersion: 4.18 diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index e9cb7372a2ca..05dab29a22f7 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -6,6 +6,7 @@ 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 diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c index ede483573fe0..21db9646443e 100644 --- a/drivers/platform/x86/lenovo/ideapad-laptop.c +++ b/drivers/platform/x86/lenovo/ideapad-laptop.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include "ideapad-laptop.h" +#include #include #include @@ -162,6 +164,7 @@ struct ideapad_private { struct backlight_device *blightdev; struct ideapad_dytc_priv *dytc; struct dentry *debug; + struct acpi_battery_hook battery_hook; unsigned long cfg; unsigned long r_touchpad_val; struct { @@ -589,6 +592,11 @@ static ssize_t camera_power_store(struct device *dev, static DEVICE_ATTR_RW(camera_power); +static void show_conservation_mode_deprecation_warning(struct device *dev) +{ + dev_warn_once(dev, "conservation_mode attribute has been deprecated, see charge_types.\n"); +} + static ssize_t conservation_mode_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -597,6 +605,8 @@ static ssize_t conservation_mode_show(struct device *dev, unsigned long result; int err; + show_conservation_mode_deprecation_warning(dev); + err = eval_gbmd(priv->adev->handle, &result); if (err) return err; @@ -612,6 +622,8 @@ static ssize_t conservation_mode_store(struct device *dev, bool state; int err; + show_conservation_mode_deprecation_warning(dev); + err = kstrtobool(buf, &state); if (err) return err; @@ -1973,10 +1985,90 @@ static const struct dmi_system_id ctrl_ps2_aux_port_list[] = { {} }; -static void ideapad_check_features(struct ideapad_private *priv) +static int ideapad_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct ideapad_private *priv = ext_data; + + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + return exec_sbmc(priv->adev->handle, SBMC_CONSERVATION_ON); + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + return exec_sbmc(priv->adev->handle, SBMC_CONSERVATION_OFF); + default: + return -EINVAL; + } +} + +static int ideapad_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ideapad_private *priv = ext_data; + unsigned long result; + int err; + + err = eval_gbmd(priv->adev->handle, &result); + if (err) + return err; + + if (test_bit(GBMD_CONSERVATION_STATE_BIT, &result)) + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + + return 0; +} + +static int ideapad_psy_prop_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp) +{ + return true; +} + +static const enum power_supply_property ideapad_power_supply_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const struct power_supply_ext ideapad_battery_ext = { + .name = "ideapad_laptop", + .properties = ideapad_power_supply_props, + .num_properties = ARRAY_SIZE(ideapad_power_supply_props), + .charge_types = (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)), + .get_property = ideapad_psy_ext_get_prop, + .set_property = ideapad_psy_ext_set_prop, + .property_is_writeable = ideapad_psy_prop_is_writeable, +}; + +static int ideapad_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct ideapad_private *priv = container_of(hook, struct ideapad_private, battery_hook); + + return power_supply_register_extension(battery, &ideapad_battery_ext, + &priv->platform_device->dev, priv); +} + +static int ideapad_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &ideapad_battery_ext); + + return 0; +} + +static int ideapad_check_features(struct ideapad_private *priv) { acpi_handle handle = priv->adev->handle; unsigned long val; + int err; priv->features.set_fn_lock_led = set_fn_lock_led || dmi_check_system(set_fn_lock_led_list); @@ -1991,8 +2083,16 @@ static void ideapad_check_features(struct ideapad_private *priv) if (!read_ec_data(handle, VPCCMD_R_FAN, &val)) priv->features.fan_mode = true; - if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) + if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) { priv->features.conservation_mode = true; + priv->battery_hook.add_battery = ideapad_battery_add; + priv->battery_hook.remove_battery = ideapad_battery_remove; + priv->battery_hook.name = "Ideapad Battery Extension"; + + err = devm_battery_hook_register(&priv->platform_device->dev, &priv->battery_hook); + if (err) + return err; + } if (acpi_has_method(handle, "DYTC")) priv->features.dytc = true; @@ -2027,6 +2127,8 @@ static void ideapad_check_features(struct ideapad_private *priv) } } } + + return 0; } #if IS_ENABLED(CONFIG_ACPI_WMI) @@ -2175,7 +2277,9 @@ static int ideapad_acpi_add(struct platform_device *pdev) if (err) return err; - ideapad_check_features(priv); + err = ideapad_check_features(priv); + if (err) + return err; ideapad_debugfs_init(priv); -- cgit v1.2.3 From 6418a8504187dc7f5b6f9d0649c03e362cb0664b Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 29 May 2025 11:18:37 -0700 Subject: platform/x86: thinkpad_acpi: Handle KCOV __init vs inline mismatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When KCOV is enabled all functions get instrumented, unless the __no_sanitize_coverage attribute is used. To prepare for __no_sanitize_coverage being applied to __init functions[1], we have to handle differences in how GCC's inline optimizations get resolved. For thinkpad_acpi routines, this means forcing two functions to be inline with __always_inline. Link: https://lore.kernel.org/lkml/20250523043935.2009972-11-kees@kernel.org/ [1] Signed-off-by: Kees Cook Link: https://lore.kernel.org/r/20250529181831.work.439-kees@kernel.org Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/thinkpad_acpi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c index e1c7bd06fa12..d5bfaa3d0de5 100644 --- a/drivers/platform/x86/lenovo/thinkpad_acpi.c +++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c @@ -559,12 +559,12 @@ static unsigned long __init tpacpi_check_quirks( return 0; } -static inline bool __pure __init tpacpi_is_lenovo(void) +static __always_inline bool __pure __init tpacpi_is_lenovo(void) { return thinkpad_id.vendor == PCI_VENDOR_ID_LENOVO; } -static inline bool __pure __init tpacpi_is_ibm(void) +static __always_inline bool __pure __init tpacpi_is_ibm(void) { return thinkpad_id.vendor == PCI_VENDOR_ID_IBM; } -- cgit v1.2.3 From 05651018f04ab00a84da8a47feb3a605ec1c7e41 Mon Sep 17 00:00:00 2001 From: Thomas Richard Date: Mon, 9 Jun 2025 14:34:49 +0200 Subject: platform/x86: lenovo-yoga-tab2-pro-1380-fastcharger: Use devm_pinctrl_register_mappings() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use devm_pinctrl_register_mappings(), so the core automatically unregisters the pinctrl mappings. It makes the code easier to read. Signed-off-by: Thomas Richard Reviewed-by: Hans de Goede Link: https://lore.kernel.org/r/20250609-lenovo-yoga-tab2-pro-1380-fastcharger-devm-pinctrl-register-mappings-v1-1-fb601f2b80f6@bootlin.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- .../x86/lenovo/yoga-tab2-pro-1380-fastcharger.c | 33 ++++++++-------------- 1 file changed, 11 insertions(+), 22 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c index b3fd6a35052a..1b33c977f6d7 100644 --- a/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c +++ b/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c @@ -240,30 +240,25 @@ static int yt2_1380_fc_pdev_probe(struct platform_device *pdev) int ret; /* Register pinctrl mappings for setting the UART3 pins mode */ - ret = pinctrl_register_mappings(yt2_1380_fc_pinctrl_map, - ARRAY_SIZE(yt2_1380_fc_pinctrl_map)); + 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)) { - ret = PTR_ERR(ctrl_dev); - goto out_pinctrl_unregister_mappings; - } + 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) { - ret = -ENOMEM; - goto out_pinctrl_unregister_mappings; - } + if (!serdev) + return -ENOMEM; ret = serdev_device_add(serdev); if (ret) { - dev_err_probe(&pdev->dev, ret, "adding serdev\n"); serdev_device_put(serdev); - goto out_pinctrl_unregister_mappings; + return dev_err_probe(&pdev->dev, ret, "adding serdev\n"); } /* @@ -273,20 +268,15 @@ static int yt2_1380_fc_pdev_probe(struct platform_device *pdev) 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 */ - ret = (ret == -EAGAIN) ? -EPROBE_DEFER : ret; - dev_err_probe(&pdev->dev, ret, "attaching serdev driver\n"); - goto out_serdev_device_remove; + 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; - -out_serdev_device_remove: - serdev_device_remove(serdev); -out_pinctrl_unregister_mappings: - pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); - return ret; } static void yt2_1380_fc_pdev_remove(struct platform_device *pdev) @@ -294,7 +284,6 @@ static void yt2_1380_fc_pdev_remove(struct platform_device *pdev) struct serdev_device *serdev = platform_get_drvdata(pdev); serdev_device_remove(serdev); - pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map); } static struct platform_driver yt2_1380_fc_pdev_driver = { -- cgit v1.2.3 From 5a7c909a53875e9c0c64cdf8e52b5716d8a74523 Mon Sep 17 00:00:00 2001 From: Bartosz Golaszewski Date: Thu, 12 Jun 2025 09:48:35 +0200 Subject: platform/x86: silicom: remove unnecessary GPIO line direction check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As of commit 92ac7de3175e3 ("gpiolib: don't allow setting values on input lines"), the GPIO core makes sure values cannot be set on input lines. Remove the unnecessary check. Signed-off-by: Bartosz Golaszewski Link: https://lore.kernel.org/r/20250612074835.13800-1-brgl@bgdev.pl Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/silicom-platform.c | 4 ---- 1 file changed, 4 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/silicom-platform.c b/drivers/platform/x86/silicom-platform.c index 021f3fed197a..63b5da410ed5 100644 --- a/drivers/platform/x86/silicom-platform.c +++ b/drivers/platform/x86/silicom-platform.c @@ -248,13 +248,9 @@ static int silicom_gpio_direction_input(struct gpio_chip *gc, static int silicom_gpio_set(struct gpio_chip *gc, unsigned int offset, int value) { - int direction = silicom_gpio_get_direction(gc, offset); u8 *channels = gpiochip_get_data(gc); int channel = channels[offset]; - if (direction == GPIO_LINE_DIRECTION_IN) - return -EPERM; - silicom_mec_port_set(channel, !value); return 0; -- cgit v1.2.3 From d9926f09edabd81db86768fd87eff3e163f109e2 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 9 Jun 2025 12:21:13 +0200 Subject: platform/x86: fujitsu: use unsigned int for kstrtounit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The charge control threshold value ranges from 0-100. Signed-off-by: Jelle van der Waa Reviewed-by: Hans de Goede Acked-by: Jonathan Woithe Link: https://lore.kernel.org/r/20250609102115.36936-2-jvanderwaa@redhat.com [ij: use reverse xmas-tree order] Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/fujitsu-laptop.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/fujitsu-laptop.c b/drivers/platform/x86/fujitsu-laptop.c index 162809140f68..c6ec61518160 100644 --- a/drivers/platform/x86/fujitsu-laptop.c +++ b/drivers/platform/x86/fujitsu-laptop.c @@ -180,7 +180,8 @@ static ssize_t charge_control_end_threshold_store(struct device *dev, const char *buf, size_t count) { int cc_end_value, s006_cc_return; - int value, ret; + unsigned int value; + int ret; ret = kstrtouint(buf, 10, &value); if (ret) -- cgit v1.2.3 From dce77641056ea4fb2efab0a66d6929a5008d7235 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 9 Jun 2025 12:21:14 +0200 Subject: platform/x86: fujitsu: clamp charge_control_end_threshold values to 50 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the sysfs ABI documentation that drivers should round written values to the nearest supported value instead of returning an error. Tested on a Fujitsu Lifebook U7720. Signed-off-by: Jelle van der Waa Reviewed-by: Hans de Goede Acked-by: Jonathan Woithe Link: https://lore.kernel.org/r/20250609102115.36936-3-jvanderwaa@redhat.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/fujitsu-laptop.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/fujitsu-laptop.c b/drivers/platform/x86/fujitsu-laptop.c index c6ec61518160..931fbcdd21b8 100644 --- a/drivers/platform/x86/fujitsu-laptop.c +++ b/drivers/platform/x86/fujitsu-laptop.c @@ -187,9 +187,12 @@ static ssize_t charge_control_end_threshold_store(struct device *dev, if (ret) return ret; - if (value < 50 || value > 100) + if (value > 100) return -EINVAL; + if (value < 50) + value = 50; + cc_end_value = value * 0x100 + 0x20; s006_cc_return = call_fext_func(fext, FUNC_S006_METHOD, CHARGE_CONTROL_RW, cc_end_value, 0x0); -- cgit v1.2.3 From d2b16853ad704ac7e1550d6faacdc53925494ebf Mon Sep 17 00:00:00 2001 From: Jerome Brunet Date: Tue, 10 Jun 2025 10:41:07 +0200 Subject: platform: arm64: lenovo-yoga-c630: use the auxiliary device creation helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auxiliary device creation of this driver is simple enough to use the available auxiliary device creation helper. Use it and remove some boilerplate code. Signed-off-by: Jerome Brunet Reviewed-by: Bryan O'Donoghue Link: https://lore.kernel.org/r/20250610-yoga-aux-v1-1-d6115aa1683c@baylibre.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/arm64/lenovo-yoga-c630.c | 40 +++---------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/arm64/lenovo-yoga-c630.c b/drivers/platform/arm64/lenovo-yoga-c630.c index 1f05c9a6a89d..75060c842b24 100644 --- a/drivers/platform/arm64/lenovo-yoga-c630.c +++ b/drivers/platform/arm64/lenovo-yoga-c630.c @@ -191,50 +191,16 @@ void yoga_c630_ec_unregister_notify(struct yoga_c630_ec *ec, struct notifier_blo } EXPORT_SYMBOL_GPL(yoga_c630_ec_unregister_notify); -static void yoga_c630_aux_release(struct device *dev) -{ - struct auxiliary_device *adev = to_auxiliary_dev(dev); - - kfree(adev); -} - -static void yoga_c630_aux_remove(void *data) -{ - struct auxiliary_device *adev = data; - - auxiliary_device_delete(adev); - auxiliary_device_uninit(adev); -} - static int yoga_c630_aux_init(struct device *parent, const char *name, struct yoga_c630_ec *ec) { struct auxiliary_device *adev; - int ret; - adev = kzalloc(sizeof(*adev), GFP_KERNEL); + adev = devm_auxiliary_device_create(parent, name, ec); if (!adev) - return -ENOMEM; - - adev->name = name; - adev->id = 0; - adev->dev.parent = parent; - adev->dev.release = yoga_c630_aux_release; - adev->dev.platform_data = ec; - - ret = auxiliary_device_init(adev); - if (ret) { - kfree(adev); - return ret; - } - - ret = auxiliary_device_add(adev); - if (ret) { - auxiliary_device_uninit(adev); - return ret; - } + return -ENODEV; - return devm_add_action_or_reset(parent, yoga_c630_aux_remove, adev); + return 0; } static int yoga_c630_ec_probe(struct i2c_client *client) -- cgit v1.2.3 From 73f0f2b52c5ea67b3140b23f58d8079d158839c8 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 10 Jun 2025 07:55:26 +0200 Subject: platform/x86: wmi: Fix WMI device naming issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple WMI devices with the same GUID are present inside a given system, the WMI driver core might fail to register all of them. Consider the following scenario: WMI devices ([-]): 05901221-D566-11D1-B2F0-00A0C9062910 (on PNP0C14:00) 05901221-D566-11D1-B2F0-00A0C9062910-1 (on PNP0C14:01) If the WMI core driver somehow unbinds from PNP0C14:00, the following will happen upon rebinding: 1. The WMI driver core counts all registered WMI devices with a GUID of 05901221-D566-11D1-B2F0-00A0C9062910 (count: 1). 2. The new WMI device will be named "05901221-D566-11D1-B2F0-00A0C9062910-1" because another device with the same GUID is already registered (on PNP0C14:01). 3. The new WMI device cannot be registered due to a name conflict. Use a IDA when building the WMI device name to avoid such name collisions by ensuring that a given WMI device ID is not reused. Userspace applications using udev for WMI device detection are not impacted by this change. Additionally userspace applications that do fully support the existing naming scheme are also not impacted. Only userspace applications using hardcoded sysfs paths will break. Introduce a kconfig option for restoring the old naming scheme to give developers time to fix any compatibility issues. Tested on a Asus Prime B650-Plus. Signed-off-by: Armin Wolf Link: https://lore.kernel.org/r/20250610055526.23688-2-W_Armin@gmx.de Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/Kconfig | 9 +++++++++ drivers/platform/x86/wmi.c | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 43055df44827..49ca98df47fd 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -37,6 +37,15 @@ config ACPI_WMI It is safe to enable this driver even if your DSDT doesn't define any ACPI-WMI devices. +config ACPI_WMI_LEGACY_DEVICE_NAMES + bool "Use legacy WMI device naming scheme" + depends on ACPI_WMI + help + Say Y here to force the WMI driver core to use the old WMI device naming + scheme when creating WMI devices. Doing so might be necessary for some + userspace applications but will cause the registration of WMI devices with + the same GUID to fail in some corner cases. + config WMI_BMOF tristate "WMI embedded Binary MOF driver" depends on ACPI_WMI diff --git a/drivers/platform/x86/wmi.c b/drivers/platform/x86/wmi.c index e46453750d5f..21b7e54bd7ab 100644 --- a/drivers/platform/x86/wmi.c +++ b/drivers/platform/x86/wmi.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +75,8 @@ struct wmi_guid_count_context { int count; }; +static DEFINE_IDA(wmi_ida); + /* * If the GUID data block is marked as expensive, we must enable and * explicitily disable data collection. @@ -978,6 +981,19 @@ static int guid_count(const guid_t *guid) return context.count; } +static int wmi_dev_set_name(struct wmi_block *wblock, int count) +{ + if (IS_ENABLED(CONFIG_ACPI_WMI_LEGACY_DEVICE_NAMES)) { + if (count) + return dev_set_name(&wblock->dev.dev, "%pUL-%d", &wblock->gblock.guid, + count); + else + return dev_set_name(&wblock->dev.dev, "%pUL", &wblock->gblock.guid); + } + + return dev_set_name(&wblock->dev.dev, "%pUL-%d", &wblock->gblock.guid, wblock->dev.dev.id); +} + static int wmi_create_device(struct device *wmi_bus_dev, struct wmi_block *wblock, struct acpi_device *device) @@ -986,7 +1002,7 @@ static int wmi_create_device(struct device *wmi_bus_dev, struct acpi_device_info *info; acpi_handle method_handle; acpi_status status; - int count; + int count, ret; if (wblock->gblock.flags & ACPI_WMI_EVENT) { wblock->dev.dev.type = &wmi_type_event; @@ -1057,11 +1073,18 @@ static int wmi_create_device(struct device *wmi_bus_dev, if (count < 0) return count; - if (count) { - dev_set_name(&wblock->dev.dev, "%pUL-%d", &wblock->gblock.guid, count); + if (count) set_bit(WMI_GUID_DUPLICATED, &wblock->flags); - } else { - dev_set_name(&wblock->dev.dev, "%pUL", &wblock->gblock.guid); + + ret = ida_alloc(&wmi_ida, GFP_KERNEL); + if (ret < 0) + return ret; + + wblock->dev.dev.id = ret; + ret = wmi_dev_set_name(wblock, count); + if (ret < 0) { + ida_free(&wmi_ida, wblock->dev.dev.id); + return ret; } device_initialize(&wblock->dev.dev); @@ -1147,6 +1170,7 @@ static int parse_wdg(struct device *wmi_bus_dev, struct platform_device *pdev) dev_err(wmi_bus_dev, "failed to register %pUL\n", &wblock->gblock.guid); + ida_free(&wmi_ida, wblock->dev.dev.id); put_device(&wblock->dev.dev); } } @@ -1246,7 +1270,10 @@ static void acpi_wmi_notify_handler(acpi_handle handle, u32 event, void *context static int wmi_remove_device(struct device *dev, void *data) { + int id = dev->id; + device_unregister(dev); + ida_free(&wmi_ida, id); return 0; } -- cgit v1.2.3 From a8fc1224f2318d3e5948671d1cad458e6372d921 Mon Sep 17 00:00:00 2001 From: Hans de Goede Date: Mon, 9 Jun 2025 12:46:19 +0200 Subject: platform/x86: x86-android-tablets: Add generic_lipo_4v2_battery info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the asus_tf103c_battery_node to shared-psy-info.c and rename it to generic_lipo_4v2_battery_node. This is a preparation patch for adding ovc-capacity-table info to the battery nodes. Reviewed-by: Andy Shevchenko Signed-off-by: Hans de Goede Link: https://lore.kernel.org/r/20250609104620.25896-1-hansg@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/x86-android-tablets/asus.c | 21 +++------------------ .../x86/x86-android-tablets/shared-psy-info.c | 16 ++++++++++++++++ .../x86/x86-android-tablets/shared-psy-info.h | 1 + 3 files changed, 20 insertions(+), 18 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/x86-android-tablets/asus.c b/drivers/platform/x86/x86-android-tablets/asus.c index 7dde63b9943f..97cd14c1fd23 100644 --- a/drivers/platform/x86/x86-android-tablets/asus.c +++ b/drivers/platform/x86/x86-android-tablets/asus.c @@ -206,24 +206,9 @@ static const struct software_node asus_tf103c_touchscreen_node = { .properties = asus_tf103c_touchscreen_props, }; -static const struct property_entry asus_tf103c_battery_props[] = { - PROPERTY_ENTRY_STRING("compatible", "simple-battery"), - PROPERTY_ENTRY_STRING("device-chemistry", "lithium-ion-polymer"), - PROPERTY_ENTRY_U32("precharge-current-microamp", 256000), - PROPERTY_ENTRY_U32("charge-term-current-microamp", 128000), - PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 2048000), - PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4208000), - PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), - { } -}; - -static const struct software_node asus_tf103c_battery_node = { - .properties = asus_tf103c_battery_props, -}; - static const struct property_entry asus_tf103c_bq24190_props[] = { PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", tusb1211_chg_det_psy, 1), - PROPERTY_ENTRY_REF("monitored-battery", &asus_tf103c_battery_node), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), PROPERTY_ENTRY_U32("ti,system-minimum-microvolt", 3600000), PROPERTY_ENTRY_BOOL("omit-battery-class"), PROPERTY_ENTRY_BOOL("disable-reset"), @@ -236,7 +221,7 @@ static const struct software_node asus_tf103c_bq24190_node = { static const struct property_entry asus_tf103c_ug3105_props[] = { PROPERTY_ENTRY_STRING_ARRAY_LEN("supplied-from", bq24190_psy, 1), - PROPERTY_ENTRY_REF("monitored-battery", &asus_tf103c_battery_node), + PROPERTY_ENTRY_REF("monitored-battery", &generic_lipo_4v2_battery_node), PROPERTY_ENTRY_U32("upisemi,rsns-microohm", 5000), { } }; @@ -321,6 +306,6 @@ const struct x86_dev_info asus_tf103c_info __initconst = { .gpio_button = &asus_me176c_tf103c_lid, .gpio_button_count = 1, .gpiod_lookup_tables = asus_tf103c_gpios, - .bat_swnode = &asus_tf103c_battery_node, + .bat_swnode = &generic_lipo_4v2_battery_node, .modules = bq24190_modules, }; diff --git a/drivers/platform/x86/x86-android-tablets/shared-psy-info.c b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c index a46fa15acfb1..55da57477153 100644 --- a/drivers/platform/x86/x86-android-tablets/shared-psy-info.c +++ b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c @@ -39,6 +39,22 @@ const struct software_node fg_bq25890_supply_node = { .properties = fg_bq25890_supply_props, }; +/* Standard LiPo (max 4.2V) settings used by most devs with a LiPo battery */ +static const struct property_entry generic_lipo_4v2_battery_props[] = { + PROPERTY_ENTRY_STRING("compatible", "simple-battery"), + PROPERTY_ENTRY_STRING("device-chemistry", "lithium-ion-polymer"), + PROPERTY_ENTRY_U32("precharge-current-microamp", 256000), + PROPERTY_ENTRY_U32("charge-term-current-microamp", 128000), + PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 2048000), + PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4208000), + PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), + { } +}; + +const struct software_node generic_lipo_4v2_battery_node = { + .properties = generic_lipo_4v2_battery_props, +}; + /* LiPo HighVoltage (max 4.35V) settings used by most devs with a HV battery */ static const struct property_entry generic_lipo_hv_4v35_battery_props[] = { PROPERTY_ENTRY_STRING("compatible", "simple-battery"), diff --git a/drivers/platform/x86/x86-android-tablets/shared-psy-info.h b/drivers/platform/x86/x86-android-tablets/shared-psy-info.h index c2d2968cddc2..bcf9845ad275 100644 --- a/drivers/platform/x86/x86-android-tablets/shared-psy-info.h +++ b/drivers/platform/x86/x86-android-tablets/shared-psy-info.h @@ -21,6 +21,7 @@ extern const char * const bq25890_psy[]; extern const struct software_node fg_bq24190_supply_node; extern const struct software_node fg_bq25890_supply_node; +extern const struct software_node generic_lipo_4v2_battery_node; extern const struct software_node generic_lipo_hv_4v35_battery_node; extern struct bq24190_platform_data bq24190_pdata; -- cgit v1.2.3 From be91bf40a96d567973d5c5e870d1464eb51b6c42 Mon Sep 17 00:00:00 2001 From: Hans de Goede Date: Mon, 9 Jun 2025 12:46:20 +0200 Subject: platform/x86: x86-android-tablets: Add ovc-capacity-table info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ovc-capacity-table info to the generic battery nodes. The values come from the ug3105 driver which currently hardcodes these values. The ug3105 driver will be modified to stop hardcoding this and instead get the values from device-properties. Reviewed-by: Andy Shevchenko Signed-off-by: Hans de Goede Link: https://lore.kernel.org/r/20250609104620.25896-2-hansg@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- .../x86/x86-android-tablets/shared-psy-info.c | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/x86-android-tablets/shared-psy-info.c b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c index 55da57477153..fe34cedb6257 100644 --- a/drivers/platform/x86/x86-android-tablets/shared-psy-info.c +++ b/drivers/platform/x86/x86-android-tablets/shared-psy-info.c @@ -39,6 +39,58 @@ const struct software_node fg_bq25890_supply_node = { .properties = fg_bq25890_supply_props, }; +static const u32 generic_lipo_battery_ovc_cap_celcius[] = { 25 }; + +static const u32 generic_lipo_4v2_battery_ovc_cap_table0[] = { + 4200000, 100, + 4150000, 95, + 4110000, 90, + 4075000, 85, + 4020000, 80, + 3982500, 75, + 3945000, 70, + 3907500, 65, + 3870000, 60, + 3853333, 55, + 3836667, 50, + 3820000, 45, + 3803333, 40, + 3786667, 35, + 3770000, 30, + 3750000, 25, + 3730000, 20, + 3710000, 15, + 3690000, 10, + 3610000, 5, + 3350000, 0 +}; + +static const u32 generic_lipo_hv_4v35_battery_ovc_cap_table0[] = { + 4300000, 100, + 4250000, 96, + 4200000, 91, + 4150000, 86, + 4110000, 82, + 4075000, 77, + 4020000, 73, + 3982500, 68, + 3945000, 64, + 3907500, 59, + 3870000, 55, + 3853333, 50, + 3836667, 45, + 3820000, 41, + 3803333, 36, + 3786667, 32, + 3770000, 27, + 3750000, 23, + 3730000, 18, + 3710000, 14, + 3690000, 9, + 3610000, 5, + 3350000, 0 +}; + /* Standard LiPo (max 4.2V) settings used by most devs with a LiPo battery */ static const struct property_entry generic_lipo_4v2_battery_props[] = { PROPERTY_ENTRY_STRING("compatible", "simple-battery"), @@ -48,6 +100,10 @@ static const struct property_entry generic_lipo_4v2_battery_props[] = { PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 2048000), PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4208000), PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-celsius", + generic_lipo_battery_ovc_cap_celcius), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-table-0", + generic_lipo_4v2_battery_ovc_cap_table0), { } }; @@ -64,6 +120,10 @@ static const struct property_entry generic_lipo_hv_4v35_battery_props[] = { PROPERTY_ENTRY_U32("constant-charge-current-max-microamp", 1856000), PROPERTY_ENTRY_U32("constant-charge-voltage-max-microvolt", 4352000), PROPERTY_ENTRY_U32("factory-internal-resistance-micro-ohms", 150000), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-celsius", + generic_lipo_battery_ovc_cap_celcius), + PROPERTY_ENTRY_U32_ARRAY("ocv-capacity-table-0", + generic_lipo_hv_4v35_battery_ovc_cap_table0), { } }; -- cgit v1.2.3 From e521d16e76cd9ea99c585e064f4e7daf657b1451 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Tue, 1 Jul 2025 20:38:22 -0700 Subject: platform/x86: Add lenovo-wmi-helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lenovo-wmi-helpers, which provides a common wrapper function for wmidev_evaluate_method that does data validation and error handling. Reviewed-by: Alok Tiwari Reviewed-by: Armin Wolf Reviewed-by: Mario Limonciello Signed-off-by: Derek J. Clark Link: https://lore.kernel.org/r/20250702033826.1057762-3-derekjohn.clark@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 4 ++ drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-helpers.c | 74 +++++++++++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-helpers.h | 20 +++++++++ 4 files changed, 99 insertions(+) create mode 100644 drivers/platform/x86/lenovo/wmi-helpers.c create mode 100644 drivers/platform/x86/lenovo/wmi-helpers.h (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 05dab29a22f7..abb488889834 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -232,3 +232,7 @@ config YT2_1380 To compile this driver as a module, choose M here: the module will be called lenovo-yogabook. + +config LENOVO_WMI_HELPERS + tristate + depends on ACPI_WMI diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index f1aa2449293b..a347d657d235 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -12,6 +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_HELPERS) += wmi-helpers.o # Add 'lenovo' prefix to each module listed in lenovo-target-* define LENOVO_OBJ_TARGET diff --git a/drivers/platform/x86/lenovo/wmi-helpers.c b/drivers/platform/x86/lenovo/wmi-helpers.c new file mode 100644 index 000000000000..f6fef6296251 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-helpers.c @@ -0,0 +1,74 @@ +// 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 + */ + +#include +#include +#include +#include +#include +#include + +#include "wmi-helpers.h" + +/** + * 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 }; + union acpi_object *ret_obj __free(kfree) = 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; + + if (retval) { + ret_obj = output.pointer; + if (!ret_obj) + return -ENODATA; + + if (ret_obj->type != ACPI_TYPE_INTEGER) + return -ENXIO; + + *retval = (u32)ret_obj->integer.value; + } + + return 0; +}; +EXPORT_SYMBOL_NS_GPL(lwmi_dev_evaluate_int, "LENOVO_WMI_HELPERS"); + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo WMI Helpers Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-helpers.h b/drivers/platform/x86/lenovo/wmi-helpers.h new file mode 100644 index 000000000000..20fd21749803 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-helpers.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_HELPERS_H_ +#define _LENOVO_WMI_HELPERS_H_ + +#include + +struct wmi_device; + +struct wmi_method_args_32 { + u32 arg0; + u32 arg1; +}; + +int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id, + unsigned char *buf, size_t size, u32 *retval); + +#endif /* !_LENOVO_WMI_HELPERS_H_ */ -- cgit v1.2.3 From 949bf144bdc72e87018197ae71aa4959f17885d5 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Tue, 1 Jul 2025 20:38:23 -0700 Subject: platform/x86: Add Lenovo WMI Events Driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lenovo-wmi-events driver. The events driver is designed as a general entrypoint for all Lenovo WMI Events. It acts as a notification chain head that will process event data and pass it on to registered drivers so they can react to the events. Currently only the Gamezone interface Thermal Mode Event GUID is implemented in this driver. It is documented in the Gamezone documentation. Suggested-by: Armin Wolf Reviewed-by: Alok Tiwari Reviewed-by: Armin Wolf Reviewed-by: Mario Limonciello Signed-off-by: Derek J. Clark Link: https://lore.kernel.org/r/20250702033826.1057762-4-derekjohn.clark@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 4 + drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-events.c | 196 +++++++++++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-events.h | 20 ++++ 4 files changed, 221 insertions(+) create mode 100644 drivers/platform/x86/lenovo/wmi-events.c create mode 100644 drivers/platform/x86/lenovo/wmi-events.h (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index abb488889834..95dce00f257e 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -233,6 +233,10 @@ config YT2_1380 To compile this driver as a module, choose M here: the module will be called lenovo-yogabook. +config LENOVO_WMI_EVENTS + tristate + depends on ACPI_WMI + config LENOVO_WMI_HELPERS tristate depends on ACPI_WMI diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index a347d657d235..814c4ba9bdbe 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -12,6 +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_EVENTS) += wmi-events.o lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS) += wmi-helpers.o # Add 'lenovo' prefix to each module listed in lenovo-target-* diff --git a/drivers/platform/x86/lenovo/wmi-events.c b/drivers/platform/x86/lenovo/wmi-events.c new file mode 100644 index 000000000000..0994cd7dd504 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-events.c @@ -0,0 +1,196 @@ +// 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 + */ + +#include +#include +#include +#include +#include +#include + +#include "wmi-events.h" +#include "wmi-gamezone.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, + .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 "); +MODULE_DESCRIPTION("Lenovo WMI Events Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-events.h b/drivers/platform/x86/lenovo/wmi-events.h new file mode 100644 index 000000000000..cd34e886912c --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-events.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#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_ */ -- cgit v1.2.3 From e1a5fe662b593108d14cd0481019601698f9fbe8 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Tue, 1 Jul 2025 20:38:24 -0700 Subject: platform/x86: Add Lenovo Capability Data 01 WMI Driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lenovo-wmi-capdata01 driver which provides the LENOVO_CAPABILITY_DATA_01 WMI data block that comes on "Other Mode" enabled hardware. Provides an interface for querying if a given attribute is supported by the hardware, as well as its default_value, max_value, min_value, and step increment. Reviewed-by: Alok Tiwari Reviewed-by: Armin Wolf Signed-off-by: Derek J. Clark Link: https://lore.kernel.org/r/20250702033826.1057762-5-derekjohn.clark@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 4 + drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-capdata01.c | 302 ++++++++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-capdata01.h | 25 +++ 4 files changed, 332 insertions(+) create mode 100644 drivers/platform/x86/lenovo/wmi-capdata01.c create mode 100644 drivers/platform/x86/lenovo/wmi-capdata01.h (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 95dce00f257e..1943e3a24f43 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -233,6 +233,10 @@ config YT2_1380 To compile this driver as a module, choose M here: the module will be called lenovo-yogabook. +config LENOVO_WMI_DATA01 + tristate + depends on ACPI_WMI + config LENOVO_WMI_EVENTS tristate depends on ACPI_WMI diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index 814c4ba9bdbe..1ce970e4ddb8 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -12,6 +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_EVENTS) += wmi-events.o lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS) += wmi-helpers.o diff --git a/drivers/platform/x86/lenovo/wmi-capdata01.c b/drivers/platform/x86/lenovo/wmi-capdata01.c new file mode 100644 index 000000000000..c922680b3cba --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata01.c @@ -0,0 +1,302 @@ +// 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 new file mode 100644 index 000000000000..bd06c5751f68 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata01.h @@ -0,0 +1,25 @@ +/* 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_ */ -- cgit v1.2.3 From 22024ac5366f065a7b931bee5b62e2588521c4f0 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Tue, 1 Jul 2025 20:38:25 -0700 Subject: platform/x86: Add Lenovo Gamezone WMI Driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lenovo-wmi-gamezone driver which provides the Lenovo Gamezone WMI interface that comes on Lenovo "Gaming Series" hardware. Provides ACPI platform profiles over WMI. Reviewed-by: Armin Wolf Signed-off-by: Derek J. Clark Link: https://lore.kernel.org/r/20250702033826.1057762-6-derekjohn.clark@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 14 + drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-gamezone.c | 407 +++++++++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-gamezone.h | 20 ++ 4 files changed, 442 insertions(+) create mode 100644 drivers/platform/x86/lenovo/wmi-gamezone.c create mode 100644 drivers/platform/x86/lenovo/wmi-gamezone.h (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 1943e3a24f43..9de050c637b7 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -244,3 +244,17 @@ config LENOVO_WMI_EVENTS 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. diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index 1ce970e4ddb8..0722fd7ad486 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -15,6 +15,7 @@ lenovo-target-$(CONFIG_LENOVO_WMI_CAMERA) += wmi-camera.o lenovo-target-$(CONFIG_LENOVO_WMI_DATA01) += wmi-capdata01.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 # Add 'lenovo' prefix to each module listed in lenovo-target-* define LENOVO_OBJ_TARGET diff --git a/drivers/platform/x86/lenovo/wmi-gamezone.c b/drivers/platform/x86/lenovo/wmi-gamezone.c new file mode 100644 index 000000000000..0eb7fe8222f4 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-gamezone.c @@ -0,0 +1,407 @@ +// 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wmi-events.h" +#include "wmi-gamezone.h" +#include "wmi-helpers.h" +#include "wmi-other.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 + +static BLOCKING_NOTIFIER_HEAD(gz_chain_head); + +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: + if (priv->extreme_supported) { + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + break; + } + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case LWMI_GZ_THERMAL_MODE_EXTREME: + *profile = PLATFORM_PROFILE_PERFORMANCE; + 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_BALANCED_PERFORMANCE: + mode = LWMI_GZ_THERMAL_MODE_PERFORMANCE; + break; + case PLATFORM_PROFILE_PERFORMANCE: + if (priv->extreme_supported) { + mode = LWMI_GZ_THERMAL_MODE_EXTREME; + break; + } + mode = LWMI_GZ_THERMAL_MODE_PERFORMANCE; + 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, + }, + {}, + +}; + +/** + * 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_BALANCED_PERFORMANCE, 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_om_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_IMPORT_NS("LENOVO_WMI_OTHER"); +MODULE_DEVICE_TABLE(wmi, lwmi_gz_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo GameZone WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-gamezone.h b/drivers/platform/x86/lenovo/wmi-gamezone.h new file mode 100644 index 000000000000..6b163a5eeb95 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-gamezone.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_GAMEZONE_H_ +#define _LENOVO_WMI_GAMEZONE_H_ + +enum gamezone_events_type { + LWMI_GZ_GET_THERMAL_MODE = 1, +}; + +enum thermal_mode { + 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, +}; + +#endif /* !_LENOVO_WMI_GAMEZONE_H_ */ -- cgit v1.2.3 From edc4b183b794baefb54aa0baeb810fe3ac65d826 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Tue, 1 Jul 2025 20:38:26 -0700 Subject: platform/x86: Add Lenovo Other Mode WMI Driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lenovo-wmi-other driver which provides the Lenovo "Other Mode" WMI interface that comes on some Lenovo "Gaming Series" hardware. Provides a firmware-attributes class which enables the use of tunable knobs for SPL, SPPT, and FPPT. Reviewed-by: Alok Tiwari Reviewed-by: Armin Wolf Signed-off-by: Derek J. Clark Link: https://lore.kernel.org/r/20250702033826.1057762-7-derekjohn.clark@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 15 + drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-other.c | 665 ++++++++++++++++++++++++++++++++ drivers/platform/x86/lenovo/wmi-other.h | 16 + 4 files changed, 697 insertions(+) create mode 100644 drivers/platform/x86/lenovo/wmi-other.c create mode 100644 drivers/platform/x86/lenovo/wmi-other.h (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 9de050c637b7..b76157b35296 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -258,3 +258,18 @@ config LENOVO_WMI_GAMEZONE 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 + select FW_ATTR_CLASS + select LENOVO_WMI_DATA01 + 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. diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index 0722fd7ad486..7b2128e3a214 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -16,6 +16,7 @@ lenovo-target-$(CONFIG_LENOVO_WMI_DATA01) += wmi-capdata01.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 diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c new file mode 100644 index 000000000000..2a960b278f11 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -0,0 +1,665 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo Other Mode WMI interface driver. + * + * This driver uses the fw_attributes class to expose the various WMI functions + * provided by the "Other Mode" WMI interface. This enables CPU and GPU power + * limit as well as various other attributes for devices that fall under the + * "Gaming Series" of Lenovo laptop devices. Each attribute exposed by the + * "Other Mode" interface has a corresponding Capability Data struct that + * allows the driver to probe details about the attribute such as if it is + * supported by the hardware, the default_value, max_value, min_value, and step + * increment. + * + * These attributes typically don't fit anywhere else in the sysfs and are set + * in Windows using one of Lenovo's multiple user applications. + * + * Copyright (C) 2025 Derek J. Clark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wmi-capdata01.h" +#include "wmi-events.h" +#include "wmi-gamezone.h" +#include "wmi-helpers.h" +#include "wmi-other.h" +#include "../firmware_attributes_class.h" + +#define LENOVO_OTHER_MODE_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B" + +#define LWMI_DEVICE_ID_CPU 0x01 + +#define LWMI_FEATURE_ID_CPU_SPPT 0x01 +#define LWMI_FEATURE_ID_CPU_SPL 0x02 +#define LWMI_FEATURE_ID_CPU_FPPT 0x03 + +#define LWMI_TYPE_ID_NONE 0x00 + +#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_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other" + +static BLOCKING_NOTIFIER_HEAD(om_chain_head); +static DEFINE_IDA(lwmi_om_ida); + +enum attribute_property { + DEFAULT_VAL, + MAX_VAL, + MIN_VAL, + STEP_VAL, + SUPPORTED, +}; + +struct lwmi_om_priv { + struct component_master_ops *ops; + struct cd01_list *cd01_list; /* only valid after capdata01 bind */ + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + struct notifier_block nb; + struct wmi_device *wdev; + int ida_id; +}; + +struct tunable_attr_01 { + struct capdata01 *capdata; + struct device *dev; + u32 feature_id; + u32 device_id; + u32 type_id; +}; + +static struct tunable_attr_01 ppt_pl1_spl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +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_pl3_fppt = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_FPPT, + .type_id = LWMI_TYPE_ID_NONE, +}; + +struct capdata01_attr_group { + const struct attribute_group *attr_group; + struct tunable_attr_01 *tunable_attr; +}; + +/** + * lwmi_om_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 + * lenovo-wmi-other driver notifier chain. + * + * Return: 0 on success, %-EEXIST on error. + */ +int lwmi_om_register_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&om_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_om_register_notifier, "LENOVO_WMI_OTHER"); + +/** + * lwmi_om_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 + * lenovo-wmi-other driver notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +int lwmi_om_unregister_notifier(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&om_chain_head, nb); +} +EXPORT_SYMBOL_NS_GPL(lwmi_om_unregister_notifier, "LENOVO_WMI_OTHER"); + +/** + * devm_lwmi_om_unregister_notifier() - Remove a notifier from the blocking + * notifier chain. + * @data: Void pointer to the notifier_block struct to register. + * + * Call lwmi_om_unregister_notifier to unregister the notifier block from the + * lenovo-wmi-other driver notifier chain. + * + * Return: 0 on success, %-ENOENT on error. + */ +static void devm_lwmi_om_unregister_notifier(void *data) +{ + struct notifier_block *nb = data; + + lwmi_om_unregister_notifier(nb); +} + +/** + * devm_lwmi_om_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_om_register_notifier to register the notifier block to the + * lenovo-wmi-other driver notifier chain. Then add devm_lwmi_om_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_om_register_notifier(struct device *dev, + struct notifier_block *nb) +{ + int ret; + + ret = lwmi_om_register_notifier(nb); + if (ret < 0) + return ret; + + return devm_add_action_or_reset(dev, devm_lwmi_om_unregister_notifier, + nb); +} +EXPORT_SYMBOL_NS_GPL(devm_lwmi_om_register_notifier, "LENOVO_WMI_OTHER"); + +/** + * lwmi_om_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. + */ +static int lwmi_om_notifier_call(enum thermal_mode *mode) +{ + int ret; + + ret = blocking_notifier_call_chain(&om_chain_head, + LWMI_GZ_GET_THERMAL_MODE, &mode); + if ((ret & ~NOTIFY_STOP_MASK) != NOTIFY_OK) + return -EINVAL; + + return 0; +} + +/* Attribute Methods */ + +/** + * int_type_show() - Emit the data type for an integer attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * + * Return: Number of characters written to buf. + */ +static ssize_t int_type_show(struct kobject *kobj, struct kobj_attribute *kattr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +/** + * attr_capdata01_show() - Get the value of the specified attribute property + * + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * @tunable_attr: The attribute to be read. + * @prop: The property of this attribute to be read. + * + * Retrieves the given property from the capability data 01 struct for the + * specified attribute's "custom" thermal mode. This function is intended + * to be generic so it can be called from any integer attributes "_show" + * function. + * + * If the WMI is success the sysfs attribute is notified. + * + * Return: Either number of characters written to buf, or an error code. + */ +static ssize_t attr_capdata01_show(struct kobject *kobj, + struct kobj_attribute *kattr, char *buf, + struct tunable_attr_01 *tunable_attr, + enum attribute_property prop) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct capdata01 capdata; + 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); + + ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata); + if (ret) + return ret; + + switch (prop) { + case DEFAULT_VAL: + value = capdata.default_value; + break; + case MAX_VAL: + value = capdata.max_value; + break; + case MIN_VAL: + value = capdata.min_value; + break; + case STEP_VAL: + value = capdata.step; + break; + default: + return -EINVAL; + } + + return sysfs_emit(buf, "%d\n", value); +} + +/** + * attr_current_value_store() - Set the current value of the given attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to read from, this is parsed to `int` type. + * @count: Required by sysfs attribute macros, pass in from the callee attr. + * @tunable_attr: The attribute to be stored. + * + * Sets the value of the given attribute when operating under the "custom" + * smartfan profile. The current smartfan profile is retrieved from the + * lenovo-wmi-gamezone driver and error is returned if the result is not + * "custom". This function is intended to be generic so it can be called from + * any integer attribute's "_store" function. The integer to be sent to the WMI + * method is range checked and an error code is returned if out of range. + * + * If the value is valid and WMI is success, then the sysfs attribute is + * notified. + * + * Return: Either count, or an error code. + */ +static ssize_t attr_current_value_store(struct kobject *kobj, + struct kobj_attribute *kattr, + const char *buf, size_t count, + struct tunable_attr_01 *tunable_attr) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + struct wmi_method_args_32 args; + struct capdata01 capdata; + enum thermal_mode mode; + u32 attribute_id; + u32 value; + int ret; + + ret = lwmi_om_notifier_call(&mode); + if (ret) + return ret; + + 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); + + ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata); + if (ret) + return ret; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value < capdata.min_value || value > capdata.max_value) + return -EINVAL; + + args.arg0 = attribute_id; + args.arg1 = value; + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, + (unsigned char *)&args, sizeof(args), NULL); + if (ret) + return ret; + + return count; +}; + +/** + * attr_current_value_show() - Get the current value of the given attribute + * @kobj: Pointer to the driver object. + * @kattr: Pointer to the attribute calling this function. + * @buf: The buffer to write to. + * @tunable_attr: The attribute to be read. + * + * Retrieves the value of the given attribute for the current smartfan profile. + * The current smartfan profile is retrieved from the lenovo-wmi-gamezone driver. + * This function is intended to be generic so it can be called from any integer + * attribute's "_show" function. + * + * If the WMI is success the sysfs attribute is notified. + * + * Return: Either number of characters written to buf, or an error code. + */ +static ssize_t attr_current_value_show(struct kobject *kobj, + struct kobj_attribute *kattr, char *buf, + struct tunable_attr_01 *tunable_attr) +{ + 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; + + 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); + + args.arg0 = attribute_id; + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (unsigned char *)&args, sizeof(args), + &retval); + if (ret) + return ret; + + return sysfs_emit(buf, "%d\n", retval); +} + +/* Lenovo WMI Other Mode Attribute macros */ +#define __LWMI_ATTR_RO(_func, _name) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _func##_##_name##_show, \ + } + +#define __LWMI_ATTR_RO_AS(_name, _show) \ + { \ + .attr = { .name = __stringify(_name), .mode = 0444 }, \ + .show = _show, \ + } + +#define __LWMI_ATTR_RW(_func, _name) \ + __ATTR(_name, 0644, _func##_##_name##_show, _func##_##_name##_store) + +/* Shows a formatted static variable */ +#define __LWMI_ATTR_SHOW_FMT(_prop, _attrname, _fmt, _val) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return sysfs_emit(buf, _fmt, _val); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __LWMI_ATTR_RO(_attrname, _prop) + +/* Attribute current value read/write */ +#define __LWMI_TUNABLE_CURRENT_VALUE_CAP01(_attrname) \ + static ssize_t _attrname##_current_value_store( \ + struct kobject *kobj, struct kobj_attribute *kattr, \ + const char *buf, size_t count) \ + { \ + return attr_current_value_store(kobj, kattr, buf, count, \ + &_attrname); \ + } \ + static ssize_t _attrname##_current_value_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return attr_current_value_show(kobj, kattr, buf, &_attrname); \ + } \ + static struct kobj_attribute attr_##_attrname##_current_value = \ + __LWMI_ATTR_RW(_attrname, current_value) + +/* Attribute property read only */ +#define __LWMI_TUNABLE_RO_CAP01(_prop, _attrname, _prop_type) \ + static ssize_t _attrname##_##_prop##_show( \ + struct kobject *kobj, struct kobj_attribute *kattr, char *buf) \ + { \ + return attr_capdata01_show(kobj, kattr, buf, &_attrname, \ + _prop_type); \ + } \ + static struct kobj_attribute attr_##_attrname##_##_prop = \ + __LWMI_ATTR_RO(_attrname, _prop) + +#define LWMI_ATTR_GROUP_TUNABLE_CAP01(_attrname, _fsname, _dispname) \ + __LWMI_TUNABLE_CURRENT_VALUE_CAP01(_attrname); \ + __LWMI_TUNABLE_RO_CAP01(default_value, _attrname, DEFAULT_VAL); \ + __LWMI_ATTR_SHOW_FMT(display_name, _attrname, "%s\n", _dispname); \ + __LWMI_TUNABLE_RO_CAP01(max_value, _attrname, MAX_VAL); \ + __LWMI_TUNABLE_RO_CAP01(min_value, _attrname, MIN_VAL); \ + __LWMI_TUNABLE_RO_CAP01(scalar_increment, _attrname, STEP_VAL); \ + static struct kobj_attribute attr_##_attrname##_type = \ + __LWMI_ATTR_RO_AS(type, int_type_show); \ + static struct attribute *_attrname##_attrs[] = { \ + &attr_##_attrname##_current_value.attr, \ + &attr_##_attrname##_default_value.attr, \ + &attr_##_attrname##_display_name.attr, \ + &attr_##_attrname##_max_value.attr, \ + &attr_##_attrname##_min_value.attr, \ + &attr_##_attrname##_scalar_increment.attr, \ + &attr_##_attrname##_type.attr, \ + NULL, \ + }; \ + static const struct attribute_group _attrname##_attr_group = { \ + .name = _fsname, .attrs = _attrname##_attrs \ + } + +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl, "ppt_pl1_spl", + "Set the CPU 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_pl3_fppt, "ppt_pl3_fppt", + "Set the CPU fast package power tracking limit"); + +static struct capdata01_attr_group cd01_attr_groups[] = { + { &ppt_pl1_spl_attr_group, &ppt_pl1_spl }, + { &ppt_pl2_sppt_attr_group, &ppt_pl2_sppt }, + { &ppt_pl3_fppt_attr_group, &ppt_pl3_fppt }, + {}, +}; + +/** + * lwmi_om_fw_attr_add() - Register all firmware_attributes_class members + * @priv: The Other Mode driver data. + * + * Return: Either 0, or an error code. + */ +static int lwmi_om_fw_attr_add(struct lwmi_om_priv *priv) +{ + unsigned int i; + int err; + + priv->ida_id = ida_alloc(&lwmi_om_ida, GFP_KERNEL); + if (priv->ida_id < 0) + return priv->ida_id; + + 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); + if (IS_ERR(priv->fw_attr_dev)) { + err = PTR_ERR(priv->fw_attr_dev); + goto err_free_ida; + } + + priv->fw_attr_kset = kset_create_and_add("attributes", NULL, + &priv->fw_attr_dev->kobj); + if (!priv->fw_attr_kset) { + err = -ENOMEM; + goto err_destroy_classdev; + } + + 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; + } + return 0; + +err_remove_groups: + while (i--) + sysfs_remove_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + + kset_unregister(priv->fw_attr_kset); + +err_destroy_classdev: + device_unregister(priv->fw_attr_dev); + +err_free_ida: + ida_free(&lwmi_om_ida, priv->ida_id); + return err; +} + +/** + * lwmi_om_fw_attr_remove() - Unregister all capability data attribute groups + * @priv: the lenovo-wmi-other driver data. + */ +static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv) +{ + for (unsigned int i = 0; i < ARRAY_SIZE(cd01_attr_groups) - 1; i++) + sysfs_remove_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + + kset_unregister(priv->fw_attr_kset); + device_unregister(priv->fw_attr_dev); +} + +/** + * 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. + * + * 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; + int ret; + + ret = component_bind_all(dev, &tmp_list); + if (ret) + return ret; + + priv->cd01_list = tmp_list; + if (!priv->cd01_list) + return -ENODEV; + + return lwmi_om_fw_attr_add(priv); +} + +/** + * 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. + */ +static void lwmi_om_master_unbind(struct device *dev) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + + lwmi_om_fw_attr_remove(priv); + component_unbind_all(dev, NULL); +} + +static const struct component_master_ops lwmi_om_master_ops = { + .bind = lwmi_om_master_bind, + .unbind = lwmi_om_master_unbind, +}; + +static int lwmi_other_probe(struct wmi_device *wdev, const void *context) +{ + struct component_match *master_match = NULL; + struct lwmi_om_priv *priv; + + priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev = wdev; + dev_set_drvdata(&wdev->dev, priv); + + component_match_add(&wdev->dev, &master_match, lwmi_cd01_match, NULL); + if (IS_ERR(master_match)) + return PTR_ERR(master_match); + + return component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops, + master_match); +} + +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); +} + +static const struct wmi_device_id lwmi_other_id_table[] = { + { LENOVO_OTHER_MODE_GUID, NULL }, + {} +}; + +static struct wmi_driver lwmi_other_driver = { + .driver = { + .name = "lenovo_wmi_other", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table = lwmi_other_id_table, + .probe = lwmi_other_probe, + .remove = lwmi_other_remove, + .no_singleton = true, +}; + +module_wmi_driver(lwmi_other_driver); + +MODULE_IMPORT_NS("LENOVO_WMI_CD01"); +MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); +MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-other.h b/drivers/platform/x86/lenovo/wmi-other.h new file mode 100644 index 000000000000..8ebf5602bb99 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-other.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_OTHER_H_ +#define _LENOVO_WMI_OTHER_H_ + +struct device; +struct notifier_block; + +int lwmi_om_register_notifier(struct notifier_block *nb); +int lwmi_om_unregister_notifier(struct notifier_block *nb); +int devm_lwmi_om_register_notifier(struct device *dev, + struct notifier_block *nb); + +#endif /* !_LENOVO_WMI_OTHER_H_ */ -- cgit v1.2.3 From dc957ab6aa05c118c3da0542428a4d6602aa2d2d Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:17 -0700 Subject: platform/x86/intel/vsec: Add private data for per-device data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new private structure, struct vsec_priv, to hold a pointer to the platform-specific information. Although the driver didn’t previously require this per-device data, adding it now lays the groundwork for upcoming patches that will manage such data. No functional changes. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-3-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 055ca9f48fb4..59fb6568a855 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -32,6 +32,10 @@ static DEFINE_IDA(intel_vsec_ida); static DEFINE_IDA(intel_vsec_sdsi_ida); static DEFINE_XARRAY_ALLOC(auxdev_array); +struct vsec_priv { + struct intel_vsec_platform_info *info; +}; + static const char *intel_vsec_name(enum intel_vsec_id id) { switch (id) { @@ -348,6 +352,7 @@ EXPORT_SYMBOL_NS_GPL(intel_vsec_register, "INTEL_VSEC"); static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct intel_vsec_platform_info *info; + struct vsec_priv *priv; bool have_devices = false; int ret; @@ -360,6 +365,13 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id if (!info) return -EINVAL; + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->info = info; + pci_set_drvdata(pdev, priv); + if (intel_vsec_walk_dvsec(pdev, info)) have_devices = true; -- cgit v1.2.3 From b0631f8a5740c55b52d02174cc4c9c84cc7a16a1 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:18 -0700 Subject: platform/x86/intel/vsec: Create wrapper to walk PCI config space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combine three PCI config space walkers — intel_vsec_walk_dvsec(), intel_vsec_walk_vsec(), and intel_vsec_walk_header() — into a new wrapper function, intel_vsec_feature_walk(). This refactoring simplifies the probe logic and lays the groundwork for future patches that will loop over these calls. No functional changes. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-4-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 59fb6568a855..8bdb74d86f24 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -349,11 +349,35 @@ int intel_vsec_register(struct pci_dev *pdev, } EXPORT_SYMBOL_NS_GPL(intel_vsec_register, "INTEL_VSEC"); +static bool intel_vsec_get_features(struct pci_dev *pdev, + struct intel_vsec_platform_info *info) +{ + bool found = false; + + /* + * Both DVSEC and VSEC capabilities can exist on the same device, + * so both intel_vsec_walk_dvsec() and intel_vsec_walk_vsec() must be + * called independently. Additionally, intel_vsec_walk_header() is + * needed for devices that do not have VSEC/DVSEC but provide the + * information via device_data. + */ + if (intel_vsec_walk_dvsec(pdev, info)) + found = true; + + if (intel_vsec_walk_vsec(pdev, info)) + found = true; + + if (info && (info->quirks & VSEC_QUIRK_NO_DVSEC) && + intel_vsec_walk_header(pdev, info)) + found = true; + + return found; +} + static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct intel_vsec_platform_info *info; struct vsec_priv *priv; - bool have_devices = false; int ret; ret = pcim_enable_device(pdev); @@ -372,17 +396,7 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id priv->info = info; pci_set_drvdata(pdev, priv); - if (intel_vsec_walk_dvsec(pdev, info)) - have_devices = true; - - if (intel_vsec_walk_vsec(pdev, info)) - have_devices = true; - - if (info && (info->quirks & VSEC_QUIRK_NO_DVSEC) && - intel_vsec_walk_header(pdev, info)) - have_devices = true; - - if (!have_devices) + if (!intel_vsec_get_features(pdev, info)) return -ENODEV; return 0; -- cgit v1.2.3 From 8a67d4b49bbdebcd255abde9e652092c3de3b657 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:19 -0700 Subject: platform/x86/intel/vsec: Add device links to enforce dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Intel VSEC features will have dependencies on other features, requiring certain supplier drivers to be probed before their consumers. To enforce this dependency ordering, introduce device links using device_link_add(), ensuring that suppliers are fully registered before consumers are probed. - Add device link tracking by storing supplier devices and tracking their state. - Implement intel_vsec_link_devices() to establish links between suppliers and consumers based on feature dependencies. - Add get_consumer_dependencies() to retrieve supplier-consumer relationships. - Modify feature registration logic: * Consumers now check that all required suppliers are registered before being initialized. * suppliers_ready() verifies that all required supplier devices are available. - Prevent potential null consumer name issue in sysfs: - Use dev_set_name() when creating auxiliary devices to ensure a unique, non-null consumer name. - Update intel_vsec_pci_probe() to loop up to the number of possible features or when all devices are registered, whichever comes first. - Introduce VSEC_CAP_UNUSED to prevent sub-features (registered via exported APIs) from being mistakenly linked. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-5-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 223 ++++++++++++++++++++++++++++++++++++-- include/linux/intel_vsec.h | 28 ++++- 2 files changed, 236 insertions(+), 15 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 8bdb74d86f24..aa1f7e63039d 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -15,9 +15,12 @@ #include #include +#include +#include #include #include #include +#include #include #include #include @@ -32,8 +35,17 @@ static DEFINE_IDA(intel_vsec_ida); static DEFINE_IDA(intel_vsec_sdsi_ida); static DEFINE_XARRAY_ALLOC(auxdev_array); +enum vsec_device_state { + STATE_NOT_FOUND, + STATE_REGISTERED, + STATE_SKIP, +}; + struct vsec_priv { struct intel_vsec_platform_info *info; + struct device *suppliers[VSEC_FEATURE_COUNT]; + enum vsec_device_state state[VSEC_FEATURE_COUNT]; + unsigned long found_caps; }; static const char *intel_vsec_name(enum intel_vsec_id id) @@ -95,6 +107,74 @@ static void intel_vsec_dev_release(struct device *dev) kfree(intel_vsec_dev); } +static const struct vsec_feature_dependency * +get_consumer_dependencies(struct vsec_priv *priv, int cap_id) +{ + const struct vsec_feature_dependency *deps = priv->info->deps; + int consumer_id = priv->info->num_deps; + + if (!deps) + return NULL; + + while (consumer_id--) + if (deps[consumer_id].feature == BIT(cap_id)) + return &deps[consumer_id]; + + return NULL; +} + +/* + * Although pci_device_id table is available in the pdev, this prototype is + * necessary because the code using it can be called by an exported API that + * might pass a different pdev. + */ +static const struct pci_device_id intel_vsec_pci_ids[]; + +static int intel_vsec_link_devices(struct pci_dev *pdev, struct device *dev, + int consumer_id) +{ + const struct vsec_feature_dependency *deps; + enum vsec_device_state *state; + struct device **suppliers; + struct vsec_priv *priv; + int supplier_id; + + if (!consumer_id) + return 0; + + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return 0; + + priv = pci_get_drvdata(pdev); + state = priv->state; + suppliers = priv->suppliers; + + priv->suppliers[consumer_id] = dev; + + deps = get_consumer_dependencies(priv, consumer_id); + if (!deps) + return 0; + + for_each_set_bit(supplier_id, &deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + struct device_link *link; + + if (state[supplier_id] != STATE_REGISTERED) + continue; + + if (!suppliers[supplier_id]) { + dev_err(dev, "Bad supplier list\n"); + return -EINVAL; + } + + link = device_link_add(dev, suppliers[supplier_id], + DL_FLAG_AUTOPROBE_CONSUMER); + if (!link) + return -EINVAL; + } + + return 0; +} + int intel_vsec_add_aux(struct pci_dev *pdev, struct device *parent, struct intel_vsec_device *intel_vsec_dev, const char *name) @@ -132,19 +212,37 @@ int intel_vsec_add_aux(struct pci_dev *pdev, struct device *parent, return ret; } + /* + * Assign a name now to ensure that the device link doesn't contain + * a null string for the consumer name. This is a problem when a supplier + * supplies more than one consumer and can lead to a duplicate name error + * when the link is created in sysfs. + */ + ret = dev_set_name(&auxdev->dev, "%s.%s.%d", KBUILD_MODNAME, auxdev->name, + auxdev->id); + if (ret) + goto cleanup_aux; + + ret = intel_vsec_link_devices(pdev, &auxdev->dev, intel_vsec_dev->cap_id); + if (ret) + goto cleanup_aux; + ret = auxiliary_device_add(auxdev); - if (ret < 0) { - auxiliary_device_uninit(auxdev); - return ret; - } + if (ret) + goto cleanup_aux; return devm_add_action_or_reset(parent, intel_vsec_remove_aux, auxdev); + +cleanup_aux: + auxiliary_device_uninit(auxdev); + return ret; } EXPORT_SYMBOL_NS_GPL(intel_vsec_add_aux, "INTEL_VSEC"); static int intel_vsec_add_dev(struct pci_dev *pdev, struct intel_vsec_header *header, - struct intel_vsec_platform_info *info) + struct intel_vsec_platform_info *info, + unsigned long cap_id) { struct intel_vsec_device __free(kfree) *intel_vsec_dev = NULL; struct resource __free(kfree) *res = NULL; @@ -211,6 +309,7 @@ static int intel_vsec_add_dev(struct pci_dev *pdev, struct intel_vsec_header *he intel_vsec_dev->quirks = info->quirks; intel_vsec_dev->base_addr = info->base_addr; intel_vsec_dev->priv_data = info->priv_data; + intel_vsec_dev->cap_id = cap_id; if (header->id == VSEC_ID_SDSI) intel_vsec_dev->ida = &intel_vsec_sdsi_ida; @@ -225,6 +324,101 @@ static int intel_vsec_add_dev(struct pci_dev *pdev, struct intel_vsec_header *he intel_vsec_name(header->id)); } +static bool suppliers_ready(struct vsec_priv *priv, + const struct vsec_feature_dependency *consumer_deps, + int cap_id) +{ + enum vsec_device_state *state = priv->state; + int supplier_id; + + if (WARN_ON_ONCE(consumer_deps->feature != BIT(cap_id))) + return false; + + /* + * Verify that all required suppliers have been found. Return false + * immediately if any are still missing. + */ + for_each_set_bit(supplier_id, &consumer_deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + if (state[supplier_id] == STATE_SKIP) + continue; + + if (state[supplier_id] == STATE_NOT_FOUND) + return false; + } + + /* + * All suppliers have been found and the consumer is ready to be + * registered. + */ + return true; +} + +static int get_cap_id(u32 header_id, unsigned long *cap_id) +{ + switch (header_id) { + case VSEC_ID_TELEMETRY: + *cap_id = ilog2(VSEC_CAP_TELEMETRY); + break; + case VSEC_ID_WATCHER: + *cap_id = ilog2(VSEC_CAP_WATCHER); + break; + case VSEC_ID_CRASHLOG: + *cap_id = ilog2(VSEC_CAP_CRASHLOG); + break; + case VSEC_ID_SDSI: + *cap_id = ilog2(VSEC_CAP_SDSI); + break; + case VSEC_ID_TPMI: + *cap_id = ilog2(VSEC_CAP_TPMI); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int intel_vsec_register_device(struct pci_dev *pdev, + struct intel_vsec_header *header, + struct intel_vsec_platform_info *info) +{ + const struct vsec_feature_dependency *consumer_deps; + struct vsec_priv *priv; + unsigned long cap_id; + int ret; + + ret = get_cap_id(header->id, &cap_id); + if (ret) + return ret; + + /* + * Only track dependencies for devices probed by the VSEC driver. + * For others using the exported APIs, add the device directly. + */ + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return intel_vsec_add_dev(pdev, header, info, cap_id); + + priv = pci_get_drvdata(pdev); + if (priv->state[cap_id] == STATE_REGISTERED || + priv->state[cap_id] == STATE_SKIP) + return -EEXIST; + + priv->found_caps |= BIT(cap_id); + + consumer_deps = get_consumer_dependencies(priv, cap_id); + if (!consumer_deps || suppliers_ready(priv, consumer_deps, cap_id)) { + ret = intel_vsec_add_dev(pdev, header, info, cap_id); + if (ret) + priv->state[cap_id] = STATE_SKIP; + else + priv->state[cap_id] = STATE_REGISTERED; + + return ret; + } + + return -EAGAIN; +} + static bool intel_vsec_walk_header(struct pci_dev *pdev, struct intel_vsec_platform_info *info) { @@ -233,7 +427,7 @@ static bool intel_vsec_walk_header(struct pci_dev *pdev, int ret; for ( ; *header; header++) { - ret = intel_vsec_add_dev(pdev, *header, info); + ret = intel_vsec_register_device(pdev, *header, info); if (!ret) have_devices = true; } @@ -281,7 +475,7 @@ static bool intel_vsec_walk_dvsec(struct pci_dev *pdev, pci_read_config_dword(pdev, pos + PCI_DVSEC_HEADER2, &hdr); header.id = PCI_DVSEC_HEADER2_ID(hdr); - ret = intel_vsec_add_dev(pdev, &header, info); + ret = intel_vsec_register_device(pdev, &header, info); if (ret) continue; @@ -326,7 +520,7 @@ static bool intel_vsec_walk_vsec(struct pci_dev *pdev, header.tbir = INTEL_DVSEC_TABLE_BAR(table); header.offset = INTEL_DVSEC_TABLE_OFFSET(table); - ret = intel_vsec_add_dev(pdev, &header, info); + ret = intel_vsec_register_device(pdev, &header, info); if (ret) continue; @@ -378,7 +572,8 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id { struct intel_vsec_platform_info *info; struct vsec_priv *priv; - int ret; + int num_caps, ret; + bool found_any = false; ret = pcim_enable_device(pdev); if (ret) @@ -396,7 +591,15 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id priv->info = info; pci_set_drvdata(pdev, priv); - if (!intel_vsec_get_features(pdev, info)) + num_caps = hweight_long(info->caps); + while (num_caps--) { + found_any |= intel_vsec_get_features(pdev, info); + + if (priv->found_caps == info->caps) + break; + } + + if (!found_any) return -ENODEV; return 0; diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index bc95821f1bfb..71067afaca99 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -5,11 +5,18 @@ #include #include -#define VSEC_CAP_TELEMETRY BIT(0) -#define VSEC_CAP_WATCHER BIT(1) -#define VSEC_CAP_CRASHLOG BIT(2) -#define VSEC_CAP_SDSI BIT(3) -#define VSEC_CAP_TPMI BIT(4) +/* + * VSEC_CAP_UNUSED is reserved. It exists to prevent zero initialized + * intel_vsec devices from being automatically set to a known + * capability with ID 0 + */ +#define VSEC_CAP_UNUSED BIT(0) +#define VSEC_CAP_TELEMETRY BIT(1) +#define VSEC_CAP_WATCHER BIT(2) +#define VSEC_CAP_CRASHLOG BIT(3) +#define VSEC_CAP_SDSI BIT(4) +#define VSEC_CAP_TPMI BIT(5) +#define VSEC_FEATURE_COUNT 6 /* Intel DVSEC offsets */ #define INTEL_DVSEC_ENTRIES 0xA @@ -81,22 +88,31 @@ struct pmt_callbacks { int (*read_telem)(struct pci_dev *pdev, u32 guid, u64 *data, loff_t off, u32 count); }; +struct vsec_feature_dependency { + unsigned long feature; + unsigned long supplier_bitmap; +}; + /** * struct intel_vsec_platform_info - Platform specific data * @parent: parent device in the auxbus chain * @headers: list of headers to define the PMT client devices to create + * @deps: array of feature dependencies * @priv_data: private data, usable by parent devices, currently a callback * @caps: bitmask of PMT capabilities for the given headers * @quirks: bitmask of VSEC device quirks * @base_addr: allow a base address to be specified (rather than derived) + * @num_deps: Count feature dependencies */ struct intel_vsec_platform_info { struct device *parent; struct intel_vsec_header **headers; + const struct vsec_feature_dependency *deps; void *priv_data; unsigned long caps; unsigned long quirks; u64 base_addr; + int num_deps; }; /** @@ -110,6 +126,7 @@ struct intel_vsec_platform_info { * @priv_data: any private data needed * @quirks: specified quirks * @base_addr: base address of entries (if specified) + * @cap_id: the enumerated id of the vsec feature */ struct intel_vsec_device { struct auxiliary_device auxdev; @@ -122,6 +139,7 @@ struct intel_vsec_device { size_t priv_data_size; unsigned long quirks; u64 base_addr; + unsigned long cap_id; }; int intel_vsec_add_aux(struct pci_dev *pdev, struct device *parent, -- cgit v1.2.3 From 1f3855ea7d6b03f68c2eec7a0bcd537cedcc6680 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:20 -0700 Subject: platform/x86/intel/vsec: Skip absent features during initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some VSEC features depend on the presence of supplier features that may not always be present. To prevent unnecessary retries and device linking during initialization, introduce logic to skip attempts to link consumers to missing suppliers. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-6-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index aa1f7e63039d..32f777b41b33 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -568,11 +568,30 @@ static bool intel_vsec_get_features(struct pci_dev *pdev, return found; } +static void intel_vsec_skip_missing_dependencies(struct pci_dev *pdev) +{ + struct vsec_priv *priv = pci_get_drvdata(pdev); + const struct vsec_feature_dependency *deps = priv->info->deps; + int consumer_id = priv->info->num_deps; + + while (consumer_id--) { + int supplier_id; + + deps = &priv->info->deps[consumer_id]; + + for_each_set_bit(supplier_id, &deps->supplier_bitmap, VSEC_FEATURE_COUNT) { + if (!(BIT(supplier_id) & priv->found_caps)) + priv->state[supplier_id] = STATE_SKIP; + } + } +} + static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct intel_vsec_platform_info *info; struct vsec_priv *priv; int num_caps, ret; + int run_once = 0; bool found_any = false; ret = pcim_enable_device(pdev); @@ -597,6 +616,11 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id if (priv->found_caps == info->caps) break; + + if (!run_once) { + intel_vsec_skip_missing_dependencies(pdev); + run_once = 1; + } } if (!found_any) -- cgit v1.2.3 From e4436e98672c7993cdfd7743efd0fcaa8df7cc17 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:21 -0700 Subject: platform/x86/intel/vsec: Skip driverless features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a feature lacks a corresponding driver and that feature is also a supplier, registering it would be prevent the consumer driver from probing. Introduces logic to skip such features during device registration. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-7-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 32f777b41b33..30e558af6888 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -123,6 +123,26 @@ get_consumer_dependencies(struct vsec_priv *priv, int cap_id) return NULL; } +static bool vsec_driver_present(int cap_id) +{ + unsigned long bit = BIT(cap_id); + + switch (bit) { + case VSEC_CAP_TELEMETRY: + return IS_ENABLED(CONFIG_INTEL_PMT_TELEMETRY); + case VSEC_CAP_WATCHER: + return IS_ENABLED(CONFIG_INTEL_PMT_WATCHER); + case VSEC_CAP_CRASHLOG: + return IS_ENABLED(CONFIG_INTEL_PMT_CRASHLOG); + case VSEC_CAP_SDSI: + return IS_ENABLED(CONFIG_INTEL_SDSI); + case VSEC_CAP_TPMI: + return IS_ENABLED(CONFIG_INTEL_TPMI); + default: + return false; + } +} + /* * Although pci_device_id table is available in the pdev, this prototype is * necessary because the code using it can be called by an exported API that @@ -158,7 +178,8 @@ static int intel_vsec_link_devices(struct pci_dev *pdev, struct device *dev, for_each_set_bit(supplier_id, &deps->supplier_bitmap, VSEC_FEATURE_COUNT) { struct device_link *link; - if (state[supplier_id] != STATE_REGISTERED) + if (state[supplier_id] != STATE_REGISTERED || + !vsec_driver_present(supplier_id)) continue; if (!suppliers[supplier_id]) { @@ -405,6 +426,11 @@ static int intel_vsec_register_device(struct pci_dev *pdev, priv->found_caps |= BIT(cap_id); + if (!vsec_driver_present(cap_id)) { + priv->state[cap_id] = STATE_SKIP; + return -ENODEV; + } + consumer_deps = get_consumer_dependencies(priv, cap_id); if (!consumer_deps || suppliers_ready(priv, consumer_deps, cap_id)) { ret = intel_vsec_add_dev(pdev, header, info, cap_id); -- cgit v1.2.3 From 10f32796e86c04f73b7f8580cc9483765ed19f49 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:22 -0700 Subject: platform/x86/intel/vsec: Add new Discovery feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the PCIe VSEC ID for new Intel Platform Monitoring Technology Capability Discovery feature. Discovery provides detailed information for the various Intel VSEC features. Also make the driver a supplier for TPMI and Telemetry drivers which will use the information. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-8-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 26 ++++++++++++++++++++++++-- include/linux/intel_vsec.h | 4 +++- 2 files changed, 27 insertions(+), 3 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 30e558af6888..4d76f1ac3c8c 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -66,6 +66,9 @@ static const char *intel_vsec_name(enum intel_vsec_id id) case VSEC_ID_TPMI: return "tpmi"; + case VSEC_ID_DISCOVERY: + return "discovery"; + default: return NULL; } @@ -84,6 +87,8 @@ static bool intel_vsec_supported(u16 id, unsigned long caps) return !!(caps & VSEC_CAP_SDSI); case VSEC_ID_TPMI: return !!(caps & VSEC_CAP_TPMI); + case VSEC_ID_DISCOVERY: + return !!(caps & VSEC_CAP_DISCOVERY); default: return false; } @@ -138,6 +143,8 @@ static bool vsec_driver_present(int cap_id) return IS_ENABLED(CONFIG_INTEL_SDSI); case VSEC_CAP_TPMI: return IS_ENABLED(CONFIG_INTEL_TPMI); + case VSEC_CAP_DISCOVERY: + return IS_ENABLED(CONFIG_INTEL_PMT_DISCOVERY); default: return false; } @@ -392,6 +399,9 @@ static int get_cap_id(u32 header_id, unsigned long *cap_id) case VSEC_ID_TPMI: *cap_id = ilog2(VSEC_CAP_TPMI); break; + case VSEC_ID_DISCOVERY: + *cap_id = ilog2(VSEC_CAP_DISCOVERY); + break; default: return -EINVAL; } @@ -681,14 +691,26 @@ static const struct intel_vsec_platform_info mtl_info = { .caps = VSEC_CAP_TELEMETRY, }; +static const struct vsec_feature_dependency oobmsm_deps[] = { + { + .feature = VSEC_CAP_TELEMETRY, + .supplier_bitmap = VSEC_CAP_DISCOVERY, + }, +}; + /* OOBMSM info */ static const struct intel_vsec_platform_info oobmsm_info = { - .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_SDSI | VSEC_CAP_TPMI, + .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_SDSI | VSEC_CAP_TPMI | + VSEC_CAP_DISCOVERY, + .deps = oobmsm_deps, + .num_deps = ARRAY_SIZE(oobmsm_deps), }; /* DMR OOBMSM info */ static const struct intel_vsec_platform_info dmr_oobmsm_info = { - .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_TPMI, + .caps = VSEC_CAP_TELEMETRY | VSEC_CAP_TPMI | VSEC_CAP_DISCOVERY, + .deps = oobmsm_deps, + .num_deps = ARRAY_SIZE(oobmsm_deps), }; /* TGL info */ diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index 71067afaca99..a07796d7d43b 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -16,7 +16,8 @@ #define VSEC_CAP_CRASHLOG BIT(3) #define VSEC_CAP_SDSI BIT(4) #define VSEC_CAP_TPMI BIT(5) -#define VSEC_FEATURE_COUNT 6 +#define VSEC_CAP_DISCOVERY BIT(6) +#define VSEC_FEATURE_COUNT 7 /* Intel DVSEC offsets */ #define INTEL_DVSEC_ENTRIES 0xA @@ -33,6 +34,7 @@ enum intel_vsec_id { VSEC_ID_TELEMETRY = 2, VSEC_ID_WATCHER = 3, VSEC_ID_CRASHLOG = 4, + VSEC_ID_DISCOVERY = 12, VSEC_ID_SDSI = 65, VSEC_ID_TPMI = 66, }; -- cgit v1.2.3 From d9a0788093565c300f7c8dd034dbfa6ac4da9aa6 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:23 -0700 Subject: platform/x86/intel/pmt: Add PMT Discovery driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch introduces a new driver to enumerate and expose Intel Platform Monitoring Technology (PMT) capabilities via a simple discovery mechanism. The PMT Discovery driver parses hardware-provided discovery tables from Intel Out of Band Management Services Modules (OOBMSM) and extracts feature information for various providers (such as TPMI, Telemetry, Crash Log, etc). This unified interface simplifies the process of determining which manageability and telemetry features are supported by a given platform. This new feature is described in the Intel Platform Monitoring Technology 3.0 specification, section 6.6 Capability. Key changes and additions: New file drivers/platform/x86/intel/pmt/discovery.c: – Implements the discovery logic to map the discovery resource, read the feature discovery table, and validate feature parameters. New file drivers/platform/x86/intel/pmt/features.c: – Defines feature names, layouts, and associated capability masks. – Provides a mapping between raw hardware attributes and sysfs representations for easier integration with user-space tools. New header include/linux/intel_pmt_features.h: – Declares constants, masks, and feature identifiers used across the PMT framework. Sysfs integration: – Feature attributes are exposed under /sys/class/intel_pmt. – Each device is represented by a subfolder within the intel_pmt class, named using its DBDF (Domain:Bus:Device.Function), e.g.: features-0000:00:03.1 – Example directory layout for a device: /sys/class/intel_pmt/features-0000:00:03.1/ ├── accelerator_telemetry ├── crash_log ├── per_core_environment_telemetry ├── per_core_performance_telemetry ├── per_rmid_energy_telemetry ├── per_rmid_perf_telemetry ├── tpmi_control ├── tracing └── uncore_telemetry By exposing PMT feature details through sysfs and integrating with the existing PMT class, this driver paves the way for more streamlined integration of PMT-based manageability and telemetry tools. Link: https://www.intel.com/content/www/us/en/content-details/710389/intel-platform-monitoring-technology-intel-pmt-external-specification.html Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-9-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/Kconfig | 12 + drivers/platform/x86/intel/pmt/Makefile | 2 + drivers/platform/x86/intel/pmt/class.c | 35 +- drivers/platform/x86/intel/pmt/class.h | 2 + drivers/platform/x86/intel/pmt/discovery.c | 602 +++++++++++++++++++++++++++++ drivers/platform/x86/intel/pmt/features.c | 205 ++++++++++ include/linux/intel_pmt_features.h | 157 ++++++++ 7 files changed, 1013 insertions(+), 2 deletions(-) create mode 100644 drivers/platform/x86/intel/pmt/discovery.c create mode 100644 drivers/platform/x86/intel/pmt/features.c create mode 100644 include/linux/intel_pmt_features.h (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/Kconfig b/drivers/platform/x86/intel/pmt/Kconfig index e916fc966221..0ad91b5112e9 100644 --- a/drivers/platform/x86/intel/pmt/Kconfig +++ b/drivers/platform/x86/intel/pmt/Kconfig @@ -38,3 +38,15 @@ config INTEL_PMT_CRASHLOG To compile this driver as a module, choose M here: the module will be called intel_pmt_crashlog. + +config INTEL_PMT_DISCOVERY + tristate "Intel Platform Monitoring Technology (PMT) Discovery driver" + depends on INTEL_VSEC + select INTEL_PMT_CLASS + help + The Intel Platform Monitoring Technology (PMT) discovery driver provides + access to details about the various PMT features and feature specific + attributes. + + To compile this driver as a module, choose M here: the module + will be called pmt_discovery. diff --git a/drivers/platform/x86/intel/pmt/Makefile b/drivers/platform/x86/intel/pmt/Makefile index 279e158c7c23..8aed7e1592e4 100644 --- a/drivers/platform/x86/intel/pmt/Makefile +++ b/drivers/platform/x86/intel/pmt/Makefile @@ -10,3 +10,5 @@ obj-$(CONFIG_INTEL_PMT_TELEMETRY) += pmt_telemetry.o pmt_telemetry-y := telemetry.o obj-$(CONFIG_INTEL_PMT_CRASHLOG) += pmt_crashlog.o pmt_crashlog-y := crashlog.o +obj-$(CONFIG_INTEL_PMT_DISCOVERY) += pmt_discovery.o +pmt_discovery-y := discovery.o features.o diff --git a/drivers/platform/x86/intel/pmt/class.c b/drivers/platform/x86/intel/pmt/class.c index 7233b654bbad..a806a81ece52 100644 --- a/drivers/platform/x86/intel/pmt/class.c +++ b/drivers/platform/x86/intel/pmt/class.c @@ -9,11 +9,13 @@ */ #include +#include #include #include #include #include #include +#include #include "class.h" @@ -166,12 +168,41 @@ static struct attribute *intel_pmt_attrs[] = { &dev_attr_offset.attr, NULL }; -ATTRIBUTE_GROUPS(intel_pmt); -static struct class intel_pmt_class = { +static umode_t intel_pmt_attr_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct auxiliary_device *auxdev = to_auxiliary_dev(dev->parent); + struct intel_vsec_device *ivdev = auxdev_to_ivdev(auxdev); + + /* + * Place the discovery features folder in /sys/class/intel_pmt, but + * exclude the common attributes as they are not applicable. + */ + if (ivdev->cap_id == ilog2(VSEC_CAP_DISCOVERY)) + return 0; + + return attr->mode; +} + +static bool intel_pmt_group_visible(struct kobject *kobj) +{ + return true; +} +DEFINE_SYSFS_GROUP_VISIBLE(intel_pmt); + +static const struct attribute_group intel_pmt_group = { + .attrs = intel_pmt_attrs, + .is_visible = SYSFS_GROUP_VISIBLE(intel_pmt), +}; +__ATTRIBUTE_GROUPS(intel_pmt); + +struct class intel_pmt_class = { .name = "intel_pmt", .dev_groups = intel_pmt_groups, }; +EXPORT_SYMBOL_GPL(intel_pmt_class); static int intel_pmt_populate_entry(struct intel_pmt_entry *entry, struct intel_vsec_device *ivdev, diff --git a/drivers/platform/x86/intel/pmt/class.h b/drivers/platform/x86/intel/pmt/class.h index b2006d57779d..39c32357ee2c 100644 --- a/drivers/platform/x86/intel/pmt/class.h +++ b/drivers/platform/x86/intel/pmt/class.h @@ -20,6 +20,7 @@ #define GET_ADDRESS(v) ((v) & GENMASK(31, 3)) struct pci_dev; +extern struct class intel_pmt_class; struct telem_endpoint { struct pci_dev *pcidev; @@ -48,6 +49,7 @@ struct intel_pmt_entry { unsigned long base_addr; size_t size; u32 guid; + u32 num_rmids; /* Number of Resource Monitoring IDs */ int devid; }; diff --git a/drivers/platform/x86/intel/pmt/discovery.c b/drivers/platform/x86/intel/pmt/discovery.c new file mode 100644 index 000000000000..4b4fa3137ad2 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/discovery.c @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Discovery driver + * + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "class.h" + +#define MAX_FEATURE_VERSION 0 +#define DT_TBIR GENMASK(2, 0) +#define FEAT_ATTR_SIZE(x) ((x) * sizeof(u32)) +#define PMT_GUID_SIZE(x) ((x) * sizeof(u32)) +#define PMT_ACCESS_TYPE_RSVD 0xF +#define SKIP_FEATURE 1 + +struct feature_discovery_table { + u32 access_type:4; + u32 version:8; + u32 size:16; + u32 reserved:4; + u32 id; + u32 offset; + u32 reserved2; +}; + +/* Common feature table header */ +struct feature_header { + u32 attr_size:8; + u32 num_guids:8; + u32 reserved:16; +}; + +/* Feature attribute fields */ +struct caps { + u32 caps; +}; + +struct command { + u32 max_stream_size:16; + u32 max_command_size:16; +}; + +struct watcher { + u32 reserved:21; + u32 period:11; + struct command command; +}; + +struct rmid { + u32 num_rmids:16; /* Number of Resource Monitoring IDs */ + u32 reserved:16; + struct watcher watcher; +}; + +struct feature_table { + struct feature_header header; + struct caps caps; + union { + struct command command; + struct watcher watcher; + struct rmid rmid; + }; + u32 *guids; +}; + +/* For backreference in struct feature */ +struct pmt_features_priv; + +struct feature { + struct feature_table table; + struct kobject kobj; + struct pmt_features_priv *priv; + struct list_head list; + const struct attribute_group *attr_group; + enum pmt_feature_id id; +}; + +struct pmt_features_priv { + struct device *parent; + struct device *dev; + int count; + u32 mask; + struct feature feature[]; +}; + +static LIST_HEAD(pmt_feature_list); +static DEFINE_MUTEX(feature_list_lock); + +#define to_pmt_feature(x) container_of(x, struct feature, kobj) +static void pmt_feature_release(struct kobject *kobj) +{ +} + +static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct pmt_cap **pmt_caps; + u32 caps = feature->table.caps.caps; + ssize_t ret = 0; + + switch (feature->id) { + case FEATURE_PER_CORE_PERF_TELEM: + pmt_caps = pmt_caps_pcpt; + break; + case FEATURE_PER_CORE_ENV_TELEM: + pmt_caps = pmt_caps_pcet; + break; + case FEATURE_PER_RMID_PERF_TELEM: + pmt_caps = pmt_caps_rmid_perf; + break; + case FEATURE_ACCEL_TELEM: + pmt_caps = pmt_caps_accel; + break; + case FEATURE_UNCORE_TELEM: + pmt_caps = pmt_caps_uncore; + break; + case FEATURE_CRASH_LOG: + pmt_caps = pmt_caps_crashlog; + break; + case FEATURE_PETE_LOG: + pmt_caps = pmt_caps_pete; + break; + case FEATURE_TPMI_CTRL: + pmt_caps = pmt_caps_tpmi; + break; + case FEATURE_TRACING: + pmt_caps = pmt_caps_tracing; + break; + case FEATURE_PER_RMID_ENERGY_TELEM: + pmt_caps = pmt_caps_rmid_energy; + break; + default: + return -EINVAL; + } + + while (*pmt_caps) { + struct pmt_cap *pmt_cap = *pmt_caps; + + while (pmt_cap->name) { + ret += sysfs_emit_at(buf, ret, "%-40s Available: %s\n", pmt_cap->name, + str_yes_no(pmt_cap->mask & caps)); + pmt_cap++; + } + pmt_caps++; + } + + return ret; +} +static struct kobj_attribute caps_attribute = __ATTR_RO(caps); + +static struct watcher *get_watcher(struct feature *feature) +{ + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + return &feature->table.rmid.watcher; + case LAYOUT_WATCHER: + return &feature->table.watcher; + default: + return ERR_PTR(-EINVAL); + } +} + +static struct command *get_command(struct feature *feature) +{ + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + return &feature->table.rmid.watcher.command; + case LAYOUT_WATCHER: + return &feature->table.watcher.command; + case LAYOUT_COMMAND: + return &feature->table.command; + default: + return ERR_PTR(-EINVAL); + } +} + +static ssize_t num_rmids_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + + return sysfs_emit(buf, "%u\n", feature->table.rmid.num_rmids); +} +static struct kobj_attribute num_rmids_attribute = __ATTR_RO(num_rmids); + +static ssize_t min_watcher_period_ms_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct watcher *watcher = get_watcher(feature); + + if (IS_ERR(watcher)) + return PTR_ERR(watcher); + + return sysfs_emit(buf, "%u\n", watcher->period); +} +static struct kobj_attribute min_watcher_period_ms_attribute = + __ATTR_RO(min_watcher_period_ms); + +static ssize_t max_stream_size_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct command *command = get_command(feature); + + if (IS_ERR(command)) + return PTR_ERR(command); + + return sysfs_emit(buf, "%u\n", command->max_stream_size); +} +static struct kobj_attribute max_stream_size_attribute = + __ATTR_RO(max_stream_size); + +static ssize_t max_command_size_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + struct command *command = get_command(feature); + + if (IS_ERR(command)) + return PTR_ERR(command); + + return sysfs_emit(buf, "%u\n", command->max_command_size); +} +static struct kobj_attribute max_command_size_attribute = + __ATTR_RO(max_command_size); + +static ssize_t guids_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct feature *feature = to_pmt_feature(kobj); + int i, count = 0; + + for (i = 0; i < feature->table.header.num_guids; i++) + count += sysfs_emit_at(buf, count, "0x%x\n", + feature->table.guids[i]); + + return count; +} +static struct kobj_attribute guids_attribute = __ATTR_RO(guids); + +static struct attribute *pmt_feature_rmid_attrs[] = { + &caps_attribute.attr, + &num_rmids_attribute.attr, + &min_watcher_period_ms_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_rmid); + +static const struct kobj_type pmt_feature_rmid_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_rmid_groups, +}; + +static struct attribute *pmt_feature_watcher_attrs[] = { + &caps_attribute.attr, + &min_watcher_period_ms_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_watcher); + +static const struct kobj_type pmt_feature_watcher_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_watcher_groups, +}; + +static struct attribute *pmt_feature_command_attrs[] = { + &caps_attribute.attr, + &max_stream_size_attribute.attr, + &max_command_size_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_command); + +static const struct kobj_type pmt_feature_command_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_command_groups, +}; + +static struct attribute *pmt_feature_guids_attrs[] = { + &caps_attribute.attr, + &guids_attribute.attr, + NULL +}; +ATTRIBUTE_GROUPS(pmt_feature_guids); + +static const struct kobj_type pmt_feature_guids_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pmt_feature_release, + .default_groups = pmt_feature_guids_groups, +}; + +static int +pmt_feature_get_disc_table(struct pmt_features_priv *priv, + struct resource *disc_res, + struct feature_discovery_table *disc_tbl) +{ + void __iomem *disc_base; + + disc_base = devm_ioremap_resource(priv->dev, disc_res); + if (IS_ERR(disc_base)) + return PTR_ERR(disc_base); + + memcpy_fromio(disc_tbl, disc_base, sizeof(*disc_tbl)); + + devm_iounmap(priv->dev, disc_base); + + if (priv->mask & BIT(disc_tbl->id)) + return dev_err_probe(priv->dev, -EINVAL, "Duplicate feature: %s\n", + pmt_feature_names[disc_tbl->id]); + + /* + * Some devices may expose non-functioning entries that are + * reserved for future use. They have zero size. Do not fail + * probe for these. Just ignore them. + */ + if (disc_tbl->size == 0 || disc_tbl->access_type == PMT_ACCESS_TYPE_RSVD) + return SKIP_FEATURE; + + if (disc_tbl->version > MAX_FEATURE_VERSION) + return SKIP_FEATURE; + + if (!pmt_feature_id_is_valid(disc_tbl->id)) + return SKIP_FEATURE; + + priv->mask |= BIT(disc_tbl->id); + + return 0; +} + +static int +pmt_feature_get_feature_table(struct pmt_features_priv *priv, + struct feature *feature, + struct feature_discovery_table *disc_tbl, + struct resource *disc_res) +{ + struct feature_table *feat_tbl = &feature->table; + struct feature_header *header; + struct resource res = {}; + resource_size_t res_size; + void __iomem *feat_base, *feat_offset; + void *tbl_offset; + size_t size; + u32 *guids; + u8 tbir; + + tbir = FIELD_GET(DT_TBIR, disc_tbl->offset); + + switch (disc_tbl->access_type) { + case ACCESS_LOCAL: + if (tbir) + return dev_err_probe(priv->dev, -EINVAL, + "Unsupported BAR index %u for access type %u\n", + tbir, disc_tbl->access_type); + + + /* + * For access_type LOCAL, the base address is as follows: + * base address = end of discovery region + base offset + 1 + */ + res = DEFINE_RES_MEM(disc_res->end + disc_tbl->offset + 1, + disc_tbl->size * sizeof(u32)); + break; + + default: + return dev_err_probe(priv->dev, -EINVAL, "Unrecognized access_type %u\n", + disc_tbl->access_type); + } + + feature->id = disc_tbl->id; + + /* Get the feature table */ + feat_base = devm_ioremap_resource(priv->dev, &res); + if (IS_ERR(feat_base)) + return PTR_ERR(feat_base); + + feat_offset = feat_base; + tbl_offset = feat_tbl; + + /* Get the header */ + header = &feat_tbl->header; + memcpy_fromio(header, feat_offset, sizeof(*header)); + + /* Validate fields fit within mapped resource */ + size = sizeof(*header) + FEAT_ATTR_SIZE(header->attr_size) + + PMT_GUID_SIZE(header->num_guids); + res_size = resource_size(&res); + if (WARN(size > res_size, "Bad table size %ld > %pa", size, &res_size)) + return -EINVAL; + + /* Get the feature attributes, including capability fields */ + tbl_offset += sizeof(*header); + feat_offset += sizeof(*header); + + memcpy_fromio(tbl_offset, feat_offset, FEAT_ATTR_SIZE(header->attr_size)); + + /* Finally, get the guids */ + guids = devm_kmalloc(priv->dev, PMT_GUID_SIZE(header->num_guids), GFP_KERNEL); + if (!guids) + return -ENOMEM; + + feat_offset += FEAT_ATTR_SIZE(header->attr_size); + + memcpy_fromio(guids, feat_offset, PMT_GUID_SIZE(header->num_guids)); + + feat_tbl->guids = guids; + + devm_iounmap(priv->dev, feat_base); + + return 0; +} + +static void pmt_features_add_feat(struct feature *feature) +{ + guard(mutex)(&feature_list_lock); + list_add(&feature->list, &pmt_feature_list); +} + +static void pmt_features_remove_feat(struct feature *feature) +{ + guard(mutex)(&feature_list_lock); + list_del(&feature->list); +} + +/* Get the discovery table and use it to get the feature table */ +static int pmt_features_discovery(struct pmt_features_priv *priv, + struct feature *feature, + struct intel_vsec_device *ivdev, + int idx) +{ + struct feature_discovery_table disc_tbl = {}; /* Avoid false warning */ + struct resource *disc_res = &ivdev->resource[idx]; + const struct kobj_type *ktype; + int ret; + + ret = pmt_feature_get_disc_table(priv, disc_res, &disc_tbl); + if (ret) + return ret; + + ret = pmt_feature_get_feature_table(priv, feature, &disc_tbl, disc_res); + if (ret) + return ret; + + switch (feature_layout[feature->id]) { + case LAYOUT_RMID: + ktype = &pmt_feature_rmid_ktype; + feature->attr_group = &pmt_feature_rmid_group; + break; + case LAYOUT_WATCHER: + ktype = &pmt_feature_watcher_ktype; + feature->attr_group = &pmt_feature_watcher_group; + break; + case LAYOUT_COMMAND: + ktype = &pmt_feature_command_ktype; + feature->attr_group = &pmt_feature_command_group; + break; + case LAYOUT_CAPS_ONLY: + ktype = &pmt_feature_guids_ktype; + feature->attr_group = &pmt_feature_guids_group; + break; + default: + return -EINVAL; + } + + ret = kobject_init_and_add(&feature->kobj, ktype, &priv->dev->kobj, + pmt_feature_names[feature->id]); + if (ret) + return ret; + + kobject_uevent(&feature->kobj, KOBJ_ADD); + pmt_features_add_feat(feature); + + return 0; +} + +static void pmt_features_remove(struct auxiliary_device *auxdev) +{ + struct pmt_features_priv *priv = auxiliary_get_drvdata(auxdev); + int i; + + for (i = 0; i < priv->count; i++) { + struct feature *feature = &priv->feature[i]; + + pmt_features_remove_feat(feature); + sysfs_remove_group(&feature->kobj, feature->attr_group); + kobject_put(&feature->kobj); + } + + device_unregister(priv->dev); +} + +static int pmt_features_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) +{ + struct intel_vsec_device *ivdev = auxdev_to_ivdev(auxdev); + struct pmt_features_priv *priv; + size_t size; + int ret, i; + + size = struct_size(priv, feature, ivdev->num_resources); + priv = devm_kzalloc(&auxdev->dev, size, GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->parent = &ivdev->pcidev->dev; + auxiliary_set_drvdata(auxdev, priv); + + priv->dev = device_create(&intel_pmt_class, &auxdev->dev, MKDEV(0, 0), priv, + "%s-%s", "features", dev_name(priv->parent)); + if (IS_ERR(priv->dev)) + return dev_err_probe(priv->dev, PTR_ERR(priv->dev), + "Could not create %s-%s device node\n", + "features", dev_name(priv->dev)); + + /* Initialize each feature */ + for (i = 0; i < ivdev->num_resources; i++) { + struct feature *feature = &priv->feature[priv->count]; + + ret = pmt_features_discovery(priv, feature, ivdev, i); + if (ret == SKIP_FEATURE) + continue; + if (ret != 0) + goto abort_probe; + + feature->priv = priv; + priv->count++; + } + + return 0; + +abort_probe: + /* + * Only fully initialized features are tracked in priv->count, which is + * incremented only after a feature is completely set up (i.e., after + * discovery and sysfs registration). If feature initialization fails, + * the failing feature's state is local and does not require rollback. + * + * Therefore, on error, we can safely call the driver's remove() routine + * pmt_features_remove() to clean up only those features that were + * fully initialized and counted. All other resources are device-managed + * and will be cleaned up automatically during device_unregister(). + */ + pmt_features_remove(auxdev); + + return ret; +} + +static const struct auxiliary_device_id pmt_features_id_table[] = { + { .name = "intel_vsec.discovery" }, + {} +}; +MODULE_DEVICE_TABLE(auxiliary, pmt_features_id_table); + +static struct auxiliary_driver pmt_features_aux_driver = { + .id_table = pmt_features_id_table, + .remove = pmt_features_remove, + .probe = pmt_features_probe, +}; +module_auxiliary_driver(pmt_features_aux_driver); + +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMT Discovery driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("INTEL_PMT"); diff --git a/drivers/platform/x86/intel/pmt/features.c b/drivers/platform/x86/intel/pmt/features.c new file mode 100644 index 000000000000..8a39cddc75c8 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/features.c @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + * + * Author: "David E. Box" + */ + +#include +#include + +#include + +const char * const pmt_feature_names[] = { + [FEATURE_PER_CORE_PERF_TELEM] = "per_core_performance_telemetry", + [FEATURE_PER_CORE_ENV_TELEM] = "per_core_environment_telemetry", + [FEATURE_PER_RMID_PERF_TELEM] = "per_rmid_perf_telemetry", + [FEATURE_ACCEL_TELEM] = "accelerator_telemetry", + [FEATURE_UNCORE_TELEM] = "uncore_telemetry", + [FEATURE_CRASH_LOG] = "crash_log", + [FEATURE_PETE_LOG] = "pete_log", + [FEATURE_TPMI_CTRL] = "tpmi_control", + [FEATURE_TRACING] = "tracing", + [FEATURE_PER_RMID_ENERGY_TELEM] = "per_rmid_energy_telemetry", +}; +EXPORT_SYMBOL_NS_GPL(pmt_feature_names, "INTEL_PMT_DISCOVERY"); + +enum feature_layout feature_layout[] = { + [FEATURE_PER_CORE_PERF_TELEM] = LAYOUT_WATCHER, + [FEATURE_PER_CORE_ENV_TELEM] = LAYOUT_WATCHER, + [FEATURE_PER_RMID_PERF_TELEM] = LAYOUT_RMID, + [FEATURE_ACCEL_TELEM] = LAYOUT_WATCHER, + [FEATURE_UNCORE_TELEM] = LAYOUT_WATCHER, + [FEATURE_CRASH_LOG] = LAYOUT_COMMAND, + [FEATURE_PETE_LOG] = LAYOUT_COMMAND, + [FEATURE_TPMI_CTRL] = LAYOUT_CAPS_ONLY, + [FEATURE_TRACING] = LAYOUT_CAPS_ONLY, + [FEATURE_PER_RMID_ENERGY_TELEM] = LAYOUT_RMID, +}; + +struct pmt_cap pmt_cap_common[] = { + {PMT_CAP_TELEM, "telemetry"}, + {PMT_CAP_WATCHER, "watcher"}, + {PMT_CAP_CRASHLOG, "crashlog"}, + {PMT_CAP_STREAMING, "streaming"}, + {PMT_CAP_THRESHOLD, "threshold"}, + {PMT_CAP_WINDOW, "window"}, + {PMT_CAP_CONFIG, "config"}, + {PMT_CAP_TRACING, "tracing"}, + {PMT_CAP_INBAND, "inband"}, + {PMT_CAP_OOB, "oob"}, + {PMT_CAP_SECURED_CHAN, "secure_chan"}, + {PMT_CAP_PMT_SP, "pmt_sp"}, + {PMT_CAP_PMT_SP_POLICY, "pmt_sp_policy"}, + {} +}; + +struct pmt_cap pmt_cap_pcpt[] = { + {PMT_CAP_PCPT_CORE_PERF, "core_performance"}, + {PMT_CAP_PCPT_CORE_C0_RES, "core_c0_residency"}, + {PMT_CAP_PCPT_CORE_ACTIVITY, "core_activity"}, + {PMT_CAP_PCPT_CACHE_PERF, "cache_performance"}, + {PMT_CAP_PCPT_QUALITY_TELEM, "quality_telemetry"}, + {} +}; + +struct pmt_cap *pmt_caps_pcpt[] = { + pmt_cap_common, + pmt_cap_pcpt, + NULL +}; + +struct pmt_cap pmt_cap_pcet[] = { + {PMT_CAP_PCET_WORKPOINT_HIST, "workpoint_histogram"}, + {PMT_CAP_PCET_CORE_CURR_TEMP, "core_current_temp"}, + {PMT_CAP_PCET_CORE_INST_RES, "core_inst_residency"}, + {PMT_CAP_PCET_QUALITY_TELEM, "quality_telemetry"}, + {PMT_CAP_PCET_CORE_CDYN_LVL, "core_cdyn_level"}, + {PMT_CAP_PCET_CORE_STRESS_LVL, "core_stress_level"}, + {PMT_CAP_PCET_CORE_DAS, "core_digital_aging_sensor"}, + {PMT_CAP_PCET_FIVR_HEALTH, "fivr_health"}, + {PMT_CAP_PCET_ENERGY, "energy"}, + {PMT_CAP_PCET_PEM_STATUS, "pem_status"}, + {PMT_CAP_PCET_CORE_C_STATE, "core_c_state"}, + {} +}; + +struct pmt_cap *pmt_caps_pcet[] = { + pmt_cap_common, + pmt_cap_pcet, + NULL +}; + +struct pmt_cap pmt_cap_rmid_perf[] = { + {PMT_CAP_RMID_CORES_PERF, "core_performance"}, + {PMT_CAP_RMID_CACHE_PERF, "cache_performance"}, + {PMT_CAP_RMID_PERF_QUAL, "performance_quality"}, + {} +}; + +struct pmt_cap *pmt_caps_rmid_perf[] = { + pmt_cap_common, + pmt_cap_rmid_perf, + NULL +}; + +struct pmt_cap pmt_cap_accel[] = { + {PMT_CAP_ACCEL_CPM_TELEM, "content_processing_module"}, + {PMT_CAP_ACCEL_TIP_TELEM, "content_turbo_ip"}, + {} +}; + +struct pmt_cap *pmt_caps_accel[] = { + pmt_cap_common, + pmt_cap_accel, + NULL +}; + +struct pmt_cap pmt_cap_uncore[] = { + {PMT_CAP_UNCORE_IO_CA_TELEM, "io_ca"}, + {PMT_CAP_UNCORE_RMID_TELEM, "rmid"}, + {PMT_CAP_UNCORE_D2D_ULA_TELEM, "d2d_ula"}, + {PMT_CAP_UNCORE_PKGC_TELEM, "package_c"}, + {} +}; + +struct pmt_cap *pmt_caps_uncore[] = { + pmt_cap_common, + pmt_cap_uncore, + NULL +}; + +struct pmt_cap pmt_cap_crashlog[] = { + {PMT_CAP_CRASHLOG_MAN_TRIG, "manual_trigger"}, + {PMT_CAP_CRASHLOG_CORE, "core"}, + {PMT_CAP_CRASHLOG_UNCORE, "uncore"}, + {PMT_CAP_CRASHLOG_TOR, "tor"}, + {PMT_CAP_CRASHLOG_S3M, "s3m"}, + {PMT_CAP_CRASHLOG_PERSISTENCY, "persistency"}, + {PMT_CAP_CRASHLOG_CLIP_GPIO, "crashlog_in_progress"}, + {PMT_CAP_CRASHLOG_PRE_RESET, "pre_reset_extraction"}, + {PMT_CAP_CRASHLOG_POST_RESET, "post_reset_extraction"}, + {} +}; + +struct pmt_cap *pmt_caps_crashlog[] = { + pmt_cap_common, + pmt_cap_crashlog, + NULL +}; + +struct pmt_cap pmt_cap_pete[] = { + {PMT_CAP_PETE_MAN_TRIG, "manual_trigger"}, + {PMT_CAP_PETE_ENCRYPTION, "encryption"}, + {PMT_CAP_PETE_PERSISTENCY, "persistency"}, + {PMT_CAP_PETE_REQ_TOKENS, "required_tokens"}, + {PMT_CAP_PETE_PROD_ENABLED, "production_enabled"}, + {PMT_CAP_PETE_DEBUG_ENABLED, "debug_enabled"}, + {} +}; + +struct pmt_cap *pmt_caps_pete[] = { + pmt_cap_common, + pmt_cap_pete, + NULL +}; + +struct pmt_cap pmt_cap_tpmi[] = { + {PMT_CAP_TPMI_MAILBOX, "mailbox"}, + {PMT_CAP_TPMI_LOCK, "bios_lock"}, + {} +}; + +struct pmt_cap *pmt_caps_tpmi[] = { + pmt_cap_common, + pmt_cap_tpmi, + NULL +}; + +struct pmt_cap pmt_cap_tracing[] = { + {PMT_CAP_TRACE_SRAR, "srar_errors"}, + {PMT_CAP_TRACE_CORRECTABLE, "correctable_errors"}, + {PMT_CAP_TRACE_MCTP, "mctp"}, + {PMT_CAP_TRACE_MRT, "memory_resiliency"}, + {} +}; + +struct pmt_cap *pmt_caps_tracing[] = { + pmt_cap_common, + pmt_cap_tracing, + NULL +}; + +struct pmt_cap pmt_cap_rmid_energy[] = { + {PMT_CAP_RMID_ENERGY, "energy"}, + {PMT_CAP_RMID_ACTIVITY, "activity"}, + {PMT_CAP_RMID_ENERGY_QUAL, "energy_quality"}, + {} +}; + +struct pmt_cap *pmt_caps_rmid_energy[] = { + pmt_cap_common, + pmt_cap_rmid_energy, + NULL +}; diff --git a/include/linux/intel_pmt_features.h b/include/linux/intel_pmt_features.h new file mode 100644 index 000000000000..53573a4a49b7 --- /dev/null +++ b/include/linux/intel_pmt_features.h @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _FEATURES_H +#define _FEATURES_H + +#include +#include + +/* Common masks */ +#define PMT_CAP_TELEM BIT(0) +#define PMT_CAP_WATCHER BIT(1) +#define PMT_CAP_CRASHLOG BIT(2) +#define PMT_CAP_STREAMING BIT(3) +#define PMT_CAP_THRESHOLD BIT(4) +#define PMT_CAP_WINDOW BIT(5) +#define PMT_CAP_CONFIG BIT(6) +#define PMT_CAP_TRACING BIT(7) +#define PMT_CAP_INBAND BIT(8) +#define PMT_CAP_OOB BIT(9) +#define PMT_CAP_SECURED_CHAN BIT(10) + +#define PMT_CAP_PMT_SP BIT(11) +#define PMT_CAP_PMT_SP_POLICY GENMASK(17, 12) + +/* Per Core Performance Telemetry (PCPT) specific masks */ +#define PMT_CAP_PCPT_CORE_PERF BIT(18) +#define PMT_CAP_PCPT_CORE_C0_RES BIT(19) +#define PMT_CAP_PCPT_CORE_ACTIVITY BIT(20) +#define PMT_CAP_PCPT_CACHE_PERF BIT(21) +#define PMT_CAP_PCPT_QUALITY_TELEM BIT(22) + +/* Per Core Environmental Telemetry (PCET) specific masks */ +#define PMT_CAP_PCET_WORKPOINT_HIST BIT(18) +#define PMT_CAP_PCET_CORE_CURR_TEMP BIT(19) +#define PMT_CAP_PCET_CORE_INST_RES BIT(20) +#define PMT_CAP_PCET_QUALITY_TELEM BIT(21) /* Same as PMT_CAP_PCPT */ +#define PMT_CAP_PCET_CORE_CDYN_LVL BIT(22) +#define PMT_CAP_PCET_CORE_STRESS_LVL BIT(23) +#define PMT_CAP_PCET_CORE_DAS BIT(24) +#define PMT_CAP_PCET_FIVR_HEALTH BIT(25) +#define PMT_CAP_PCET_ENERGY BIT(26) +#define PMT_CAP_PCET_PEM_STATUS BIT(27) +#define PMT_CAP_PCET_CORE_C_STATE BIT(28) + +/* Per RMID Performance Telemetry specific masks */ +#define PMT_CAP_RMID_CORES_PERF BIT(18) +#define PMT_CAP_RMID_CACHE_PERF BIT(19) +#define PMT_CAP_RMID_PERF_QUAL BIT(20) + +/* Accelerator Telemetry specific masks */ +#define PMT_CAP_ACCEL_CPM_TELEM BIT(18) +#define PMT_CAP_ACCEL_TIP_TELEM BIT(19) + +/* Uncore Telemetry specific masks */ +#define PMT_CAP_UNCORE_IO_CA_TELEM BIT(18) +#define PMT_CAP_UNCORE_RMID_TELEM BIT(19) +#define PMT_CAP_UNCORE_D2D_ULA_TELEM BIT(20) +#define PMT_CAP_UNCORE_PKGC_TELEM BIT(21) + +/* Crash Log specific masks */ +#define PMT_CAP_CRASHLOG_MAN_TRIG BIT(11) +#define PMT_CAP_CRASHLOG_CORE BIT(12) +#define PMT_CAP_CRASHLOG_UNCORE BIT(13) +#define PMT_CAP_CRASHLOG_TOR BIT(14) +#define PMT_CAP_CRASHLOG_S3M BIT(15) +#define PMT_CAP_CRASHLOG_PERSISTENCY BIT(16) +#define PMT_CAP_CRASHLOG_CLIP_GPIO BIT(17) +#define PMT_CAP_CRASHLOG_PRE_RESET BIT(18) +#define PMT_CAP_CRASHLOG_POST_RESET BIT(19) + +/* PeTe Log specific masks */ +#define PMT_CAP_PETE_MAN_TRIG BIT(11) +#define PMT_CAP_PETE_ENCRYPTION BIT(12) +#define PMT_CAP_PETE_PERSISTENCY BIT(13) +#define PMT_CAP_PETE_REQ_TOKENS BIT(14) +#define PMT_CAP_PETE_PROD_ENABLED BIT(15) +#define PMT_CAP_PETE_DEBUG_ENABLED BIT(16) + +/* TPMI control specific masks */ +#define PMT_CAP_TPMI_MAILBOX BIT(11) +#define PMT_CAP_TPMI_LOCK BIT(12) + +/* Tracing specific masks */ +#define PMT_CAP_TRACE_SRAR BIT(11) +#define PMT_CAP_TRACE_CORRECTABLE BIT(12) +#define PMT_CAP_TRACE_MCTP BIT(13) +#define PMT_CAP_TRACE_MRT BIT(14) + +/* Per RMID Energy Telemetry specific masks */ +#define PMT_CAP_RMID_ENERGY BIT(18) +#define PMT_CAP_RMID_ACTIVITY BIT(19) +#define PMT_CAP_RMID_ENERGY_QUAL BIT(20) + +enum pmt_feature_id { + FEATURE_INVALID = 0x0, + FEATURE_PER_CORE_PERF_TELEM = 0x1, + FEATURE_PER_CORE_ENV_TELEM = 0x2, + FEATURE_PER_RMID_PERF_TELEM = 0x3, + FEATURE_ACCEL_TELEM = 0x4, + FEATURE_UNCORE_TELEM = 0x5, + FEATURE_CRASH_LOG = 0x6, + FEATURE_PETE_LOG = 0x7, + FEATURE_TPMI_CTRL = 0x8, + FEATURE_RESERVED = 0x9, + FEATURE_TRACING = 0xA, + FEATURE_PER_RMID_ENERGY_TELEM = 0xB, + FEATURE_MAX = 0xB, +}; + +enum feature_layout { + LAYOUT_RMID, + LAYOUT_WATCHER, + LAYOUT_COMMAND, + LAYOUT_CAPS_ONLY, +}; + +struct pmt_cap { + u32 mask; + const char *name; +}; + +extern const char * const pmt_feature_names[]; +extern enum feature_layout feature_layout[]; +extern struct pmt_cap pmt_cap_common[]; +extern struct pmt_cap pmt_cap_pcpt[]; +extern struct pmt_cap *pmt_caps_pcpt[]; +extern struct pmt_cap pmt_cap_pcet[]; +extern struct pmt_cap *pmt_caps_pcet[]; +extern struct pmt_cap pmt_cap_rmid_perf[]; +extern struct pmt_cap *pmt_caps_rmid_perf[]; +extern struct pmt_cap pmt_cap_accel[]; +extern struct pmt_cap *pmt_caps_accel[]; +extern struct pmt_cap pmt_cap_uncore[]; +extern struct pmt_cap *pmt_caps_uncore[]; +extern struct pmt_cap pmt_cap_crashlog[]; +extern struct pmt_cap *pmt_caps_crashlog[]; +extern struct pmt_cap pmt_cap_pete[]; +extern struct pmt_cap *pmt_caps_pete[]; +extern struct pmt_cap pmt_cap_tpmi[]; +extern struct pmt_cap *pmt_caps_tpmi[]; +extern struct pmt_cap pmt_cap_s3m[]; +extern struct pmt_cap *pmt_caps_s3m[]; +extern struct pmt_cap pmt_cap_tracing[]; +extern struct pmt_cap *pmt_caps_tracing[]; +extern struct pmt_cap pmt_cap_rmid_energy[]; +extern struct pmt_cap *pmt_caps_rmid_energy[]; + +static inline bool pmt_feature_id_is_valid(enum pmt_feature_id id) +{ + if (id > FEATURE_MAX) + return false; + + if (id == FEATURE_INVALID || id == FEATURE_RESERVED) + return false; + + return true; +} +#endif -- cgit v1.2.3 From 934954df0f44de5e10afc1af84c06f78149f15fe Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:25 -0700 Subject: platform/x86/intel/tpmi: Relocate platform info to intel_vsec.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TPMI platform information provides a mapping of OOBMSM PCI devices to logical CPUs. Since this mapping is consistent across all OOBMSM features (e.g., TPMI, PMT, SDSi), it can be leveraged by multiple drivers. To facilitate reuse, relocate the struct intel_tpmi_plat_info to intel_vsec.h, renaming it to struct oobmsm_plat_info, making it accessible to other features. While modifying headers, place them in alphabetical order. Signed-off-by: David E. Box Reviewed-by: Ilpo Järvinen Link: https://lore.kernel.org/r/20250703022832.1302928-11-david.e.box@linux.intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/plr_tpmi.c | 3 ++- .../x86/intel/speed_select_if/isst_tpmi_core.c | 9 ++++---- .../intel/uncore-frequency/uncore-frequency-tpmi.c | 7 +++--- drivers/platform/x86/intel/vsec_tpmi.c | 4 ++-- drivers/powercap/intel_rapl_tpmi.c | 9 ++++---- include/linux/intel_tpmi.h | 27 +++------------------- include/linux/intel_vsec.h | 22 ++++++++++++++++++ 7 files changed, 43 insertions(+), 38 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/plr_tpmi.c b/drivers/platform/x86/intel/plr_tpmi.c index 2b55347a5a93..58132da47745 100644 --- a/drivers/platform/x86/intel/plr_tpmi.c +++ b/drivers/platform/x86/intel/plr_tpmi.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -256,7 +257,7 @@ DEFINE_SHOW_STORE_ATTRIBUTE(plr_status); static int intel_plr_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) { - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; struct dentry *dentry; int i, num_resources; struct resource *res; diff --git a/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c index 18c035710eb9..34bff2f65a83 100644 --- a/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c +++ b/drivers/platform/x86/intel/speed_select_if/isst_tpmi_core.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1546,7 +1547,7 @@ int tpmi_sst_dev_add(struct auxiliary_device *auxdev) { struct tpmi_per_power_domain_info *pd_info; bool read_blocked = 0, write_blocked = 0; - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; struct device *dev = &auxdev->dev; struct tpmi_sst_struct *tpmi_sst; u8 i, num_resources, io_die_cnt; @@ -1698,7 +1699,7 @@ EXPORT_SYMBOL_NS_GPL(tpmi_sst_dev_add, "INTEL_TPMI_SST"); void tpmi_sst_dev_remove(struct auxiliary_device *auxdev) { struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; plat_info = tpmi_get_platform_data(auxdev); if (!plat_info) @@ -1720,7 +1721,7 @@ void tpmi_sst_dev_suspend(struct auxiliary_device *auxdev) { struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); struct tpmi_per_power_domain_info *power_domain_info; - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; void __iomem *cp_base; plat_info = tpmi_get_platform_data(auxdev); @@ -1748,7 +1749,7 @@ void tpmi_sst_dev_resume(struct auxiliary_device *auxdev) { struct tpmi_sst_struct *tpmi_sst = auxiliary_get_drvdata(auxdev); struct tpmi_per_power_domain_info *power_domain_info; - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; void __iomem *cp_base; plat_info = tpmi_get_platform_data(auxdev); diff --git a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c index 44d9948ed224..6df55c8e16b7 100644 --- a/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c +++ b/drivers/platform/x86/intel/uncore-frequency/uncore-frequency-tpmi.c @@ -22,9 +22,10 @@ #include #include #include +#include +#include #include #include -#include #include "../tpmi_power_domains.h" #include "uncore-frequency-common.h" @@ -448,7 +449,7 @@ static void remove_cluster_entries(struct tpmi_uncore_struct *tpmi_uncore) } static void set_cdie_id(int domain_id, struct tpmi_uncore_cluster_info *cluster_info, - struct intel_tpmi_plat_info *plat_info) + struct oobmsm_plat_info *plat_info) { cluster_info->cdie_id = domain_id; @@ -465,7 +466,7 @@ static void set_cdie_id(int domain_id, struct tpmi_uncore_cluster_info *cluster_ static int uncore_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) { bool read_blocked = 0, write_blocked = 0; - struct intel_tpmi_plat_info *plat_info; + struct oobmsm_plat_info *plat_info; struct tpmi_uncore_struct *tpmi_uncore; bool uncore_sysfs_added = false; int ret, i, pkg = 0; diff --git a/drivers/platform/x86/intel/vsec_tpmi.c b/drivers/platform/x86/intel/vsec_tpmi.c index 5c383a27bbe8..d95a0d994546 100644 --- a/drivers/platform/x86/intel/vsec_tpmi.c +++ b/drivers/platform/x86/intel/vsec_tpmi.c @@ -116,7 +116,7 @@ struct intel_tpmi_info { struct intel_vsec_device *vsec_dev; int feature_count; u64 pfs_start; - struct intel_tpmi_plat_info plat_info; + struct oobmsm_plat_info plat_info; void __iomem *tpmi_control_mem; struct dentry *dbgfs_dir; }; @@ -187,7 +187,7 @@ struct tpmi_feature_state { /* Used during auxbus device creation */ static DEFINE_IDA(intel_vsec_tpmi_ida); -struct intel_tpmi_plat_info *tpmi_get_platform_data(struct auxiliary_device *auxdev) +struct oobmsm_plat_info *tpmi_get_platform_data(struct auxiliary_device *auxdev) { struct intel_vsec_device *vsec_dev = auxdev_to_ivdev(auxdev); diff --git a/drivers/powercap/intel_rapl_tpmi.c b/drivers/powercap/intel_rapl_tpmi.c index af2368f4db10..82201bf4685d 100644 --- a/drivers/powercap/intel_rapl_tpmi.c +++ b/drivers/powercap/intel_rapl_tpmi.c @@ -9,9 +9,10 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include -#include -#include #include +#include +#include +#include #include #include @@ -48,7 +49,7 @@ enum tpmi_rapl_register { struct tpmi_rapl_package { struct rapl_if_priv priv; - struct intel_tpmi_plat_info *tpmi_info; + struct oobmsm_plat_info *tpmi_info; struct rapl_package *rp; void __iomem *base; struct list_head node; @@ -253,7 +254,7 @@ static int intel_rapl_tpmi_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) { struct tpmi_rapl_package *trp; - struct intel_tpmi_plat_info *info; + struct oobmsm_plat_info *info; struct resource *res; u32 offset; int ret; diff --git a/include/linux/intel_tpmi.h b/include/linux/intel_tpmi.h index ff480b47ae64..94c06bf214fb 100644 --- a/include/linux/intel_tpmi.h +++ b/include/linux/intel_tpmi.h @@ -8,6 +8,8 @@ #include +struct oobmsm_plat_info; + #define TPMI_VERSION_INVALID 0xff #define TPMI_MINOR_VERSION(val) FIELD_GET(GENMASK(4, 0), val) #define TPMI_MAJOR_VERSION(val) FIELD_GET(GENMASK(7, 5), val) @@ -26,30 +28,7 @@ enum intel_tpmi_id { TPMI_INFO_ID = 0x81, /* Special ID for PCI BDF and Package ID information */ }; -/** - * struct intel_tpmi_plat_info - Platform information for a TPMI device instance - * @cdie_mask: Mask of all compute dies in the partition - * @package_id: CPU Package id - * @partition: Package partition id when multiple VSEC PCI devices per package - * @segment: PCI segment ID - * @bus_number: PCI bus number - * @device_number: PCI device number - * @function_number: PCI function number - * - * Structure to store platform data for a TPMI device instance. This - * struct is used to return data via tpmi_get_platform_data(). - */ -struct intel_tpmi_plat_info { - u16 cdie_mask; - u8 package_id; - u8 partition; - u8 segment; - u8 bus_number; - u8 device_number; - u8 function_number; -}; - -struct intel_tpmi_plat_info *tpmi_get_platform_data(struct auxiliary_device *auxdev); +struct oobmsm_plat_info *tpmi_get_platform_data(struct auxiliary_device *auxdev); struct resource *tpmi_get_resource_at_index(struct auxiliary_device *auxdev, int index); int tpmi_get_resource_count(struct auxiliary_device *auxdev); int tpmi_get_feature_status(struct auxiliary_device *auxdev, int feature_id, bool *read_blocked, diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index a07796d7d43b..cd78d0b2e623 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -144,6 +144,28 @@ struct intel_vsec_device { unsigned long cap_id; }; +/** + * struct oobmsm_plat_info - Platform information for a device instance + * @cdie_mask: Mask of all compute dies in the partition + * @package_id: CPU Package id + * @partition: Package partition id when multiple VSEC PCI devices per package + * @segment: PCI segment ID + * @bus_number: PCI bus number + * @device_number: PCI device number + * @function_number: PCI function number + * + * Structure to store platform data for a OOBMSM device instance. + */ +struct oobmsm_plat_info { + u16 cdie_mask; + u8 package_id; + u8 partition; + u8 segment; + u8 bus_number; + u8 device_number; + u8 function_number; +}; + int intel_vsec_add_aux(struct pci_dev *pdev, struct device *parent, struct intel_vsec_device *intel_vsec_dev, const char *name); -- cgit v1.2.3 From a885a2780937afac4f31f00d11663f50d05dfb35 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:26 -0700 Subject: platform/x86/intel/vsec: Set OOBMSM to CPU mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add functions, intel_vsec_set/get_mapping(), to set and retrieve the OOBMSM-to-CPU mapping data in the private data of the parent Intel VSEC driver. With this mapping information available, other Intel VSEC features on the same OOBMSM device can easily access and use the mapping data, allowing each of the OOBMSM features to map to the CPUs they provides data for. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-12-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 31 +++++++++++++++++++++++++++++++ include/linux/intel_vsec.h | 12 ++++++++++++ 2 files changed, 43 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 4d76f1ac3c8c..711ff4edfe21 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -44,6 +44,7 @@ enum vsec_device_state { struct vsec_priv { struct intel_vsec_platform_info *info; struct device *suppliers[VSEC_FEATURE_COUNT]; + struct oobmsm_plat_info plat_info; enum vsec_device_state state[VSEC_FEATURE_COUNT]; unsigned long found_caps; }; @@ -665,6 +666,36 @@ static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id return 0; } +int intel_vsec_set_mapping(struct oobmsm_plat_info *plat_info, + struct intel_vsec_device *vsec_dev) +{ + struct vsec_priv *priv; + + priv = pci_get_drvdata(vsec_dev->pcidev); + if (!priv) + return -EINVAL; + + priv->plat_info = *plat_info; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_set_mapping, "INTEL_VSEC"); + +struct oobmsm_plat_info *intel_vsec_get_mapping(struct pci_dev *pdev) +{ + struct vsec_priv *priv; + + if (!pci_match_id(intel_vsec_pci_ids, pdev)) + return ERR_PTR(-EINVAL); + + priv = pci_get_drvdata(pdev); + if (!priv) + return ERR_PTR(-EINVAL); + + return &priv->plat_info; +} +EXPORT_SYMBOL_NS_GPL(intel_vsec_get_mapping, "INTEL_VSEC"); + /* DG1 info */ static struct intel_vsec_header dg1_header = { .length = 0x10, diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index cd78d0b2e623..4bd0c6e7857c 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -183,11 +183,23 @@ static inline struct intel_vsec_device *auxdev_to_ivdev(struct auxiliary_device #if IS_ENABLED(CONFIG_INTEL_VSEC) int intel_vsec_register(struct pci_dev *pdev, struct intel_vsec_platform_info *info); +int intel_vsec_set_mapping(struct oobmsm_plat_info *plat_info, + struct intel_vsec_device *vsec_dev); +struct oobmsm_plat_info *intel_vsec_get_mapping(struct pci_dev *pdev); #else static inline int intel_vsec_register(struct pci_dev *pdev, struct intel_vsec_platform_info *info) { return -ENODEV; } +static inline int intel_vsec_set_mapping(struct oobmsm_plat_info *plat_info, + struct intel_vsec_device *vsec_dev) +{ + return -ENODEV; +} +static inline struct oobmsm_plat_info *intel_vsec_get_mapping(struct pci_dev *pdev) +{ + return ERR_PTR(-ENODEV); +} #endif #endif -- cgit v1.2.3 From c9699057521834862616ce159a47bd33920f0d9f Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:27 -0700 Subject: platform/x86/intel/tpmi: Get OOBMSM CPU mapping from TPMI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy TPMI’s OOBMSM platform info into a common area within VSEC private data via intel_vsec_set_mapping(). This enables other Intel VSEC features to access the CPU mapping without additional queries. Additionally, designate the TPMI driver as a supplier for the Telemetry driver, ensuring it can obtain the necessary platform information for future feature extensions. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-13-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/vsec.c | 2 +- drivers/platform/x86/intel/vsec_tpmi.c | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/vsec.c b/drivers/platform/x86/intel/vsec.c index 711ff4edfe21..f66f0ce8559b 100644 --- a/drivers/platform/x86/intel/vsec.c +++ b/drivers/platform/x86/intel/vsec.c @@ -725,7 +725,7 @@ static const struct intel_vsec_platform_info mtl_info = { static const struct vsec_feature_dependency oobmsm_deps[] = { { .feature = VSEC_CAP_TELEMETRY, - .supplier_bitmap = VSEC_CAP_DISCOVERY, + .supplier_bitmap = VSEC_CAP_DISCOVERY | VSEC_CAP_TPMI, }, }; diff --git a/drivers/platform/x86/intel/vsec_tpmi.c b/drivers/platform/x86/intel/vsec_tpmi.c index d95a0d994546..7748b5557a18 100644 --- a/drivers/platform/x86/intel/vsec_tpmi.c +++ b/drivers/platform/x86/intel/vsec_tpmi.c @@ -799,6 +799,10 @@ static int intel_vsec_tpmi_init(struct auxiliary_device *auxdev) ret = tpmi_process_info(tpmi_info, pfs); if (ret) return ret; + + ret = intel_vsec_set_mapping(&tpmi_info->plat_info, vsec_dev); + if (ret) + return ret; } if (pfs->pfs_header.tpmi_id == TPMI_CONTROL_ID) -- cgit v1.2.3 From 86fc85c75bcd9b0f28afadd60c9f890669b42ba4 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:28 -0700 Subject: platform/x86/intel/pmt/discovery: Get telemetry attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add intel_pmt_get_features() in PMT Discovery to enable the PMT Telemetry driver to obtain attributes of the aggregated telemetry spaces it enumerates. The function gathers feature flags and associated data (like the number of RMIDs) from each PMT entry, laying the groundwork for a future kernel interface that will allow direct access to telemetry regions based on their capabilities. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-14-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/Kconfig | 1 + drivers/platform/x86/intel/pmt/class.h | 7 +++++++ drivers/platform/x86/intel/pmt/discovery.c | 33 ++++++++++++++++++++++++++++++ drivers/platform/x86/intel/pmt/telemetry.c | 5 +++++ include/linux/intel_vsec.h | 16 +++++++++++++++ 5 files changed, 62 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/Kconfig b/drivers/platform/x86/intel/pmt/Kconfig index 0ad91b5112e9..83ae17eab462 100644 --- a/drivers/platform/x86/intel/pmt/Kconfig +++ b/drivers/platform/x86/intel/pmt/Kconfig @@ -18,6 +18,7 @@ config INTEL_PMT_CLASS config INTEL_PMT_TELEMETRY tristate "Intel Platform Monitoring Technology (PMT) Telemetry driver" depends on INTEL_VSEC + select INTEL_PMT_DISCOVERY select INTEL_PMT_CLASS help The Intel Platform Monitory Technology (PMT) Telemetry driver provides diff --git a/drivers/platform/x86/intel/pmt/class.h b/drivers/platform/x86/intel/pmt/class.h index 39c32357ee2c..fdf7e79d8c0d 100644 --- a/drivers/platform/x86/intel/pmt/class.h +++ b/drivers/platform/x86/intel/pmt/class.h @@ -48,6 +48,7 @@ struct intel_pmt_entry { struct pmt_callbacks *cb; unsigned long base_addr; size_t size; + u64 feature_flags; u32 guid; u32 num_rmids; /* Number of Resource Monitoring IDs */ int devid; @@ -71,4 +72,10 @@ int intel_pmt_dev_create(struct intel_pmt_entry *entry, struct intel_vsec_device *dev, int idx); void intel_pmt_dev_destroy(struct intel_pmt_entry *entry, struct intel_pmt_namespace *ns); +#if IS_ENABLED(CONFIG_INTEL_PMT_DISCOVERY) +void intel_pmt_get_features(struct intel_pmt_entry *entry); +#else +static inline void intel_pmt_get_features(struct intel_pmt_entry *entry) {} +#endif + #endif diff --git a/drivers/platform/x86/intel/pmt/discovery.c b/drivers/platform/x86/intel/pmt/discovery.c index 4b4fa3137ad2..e72d43b675b4 100644 --- a/drivers/platform/x86/intel/pmt/discovery.c +++ b/drivers/platform/x86/intel/pmt/discovery.c @@ -583,6 +583,39 @@ abort_probe: return ret; } +static void pmt_get_features(struct intel_pmt_entry *entry, struct feature *f) +{ + int num_guids = f->table.header.num_guids; + int i; + + for (i = 0; i < num_guids; i++) { + if (f->table.guids[i] != entry->guid) + continue; + + entry->feature_flags |= BIT(f->id); + + if (feature_layout[f->id] == LAYOUT_RMID) + entry->num_rmids = f->table.rmid.num_rmids; + else + entry->num_rmids = 0; /* entry is kzalloc but set anyway */ + } +} + +void intel_pmt_get_features(struct intel_pmt_entry *entry) +{ + struct feature *feature; + + mutex_lock(&feature_list_lock); + list_for_each_entry(feature, &pmt_feature_list, list) { + if (feature->priv->parent != &entry->ep->pcidev->dev) + continue; + + pmt_get_features(entry, feature); + } + mutex_unlock(&feature_list_lock); +} +EXPORT_SYMBOL_NS_GPL(intel_pmt_get_features, "INTEL_PMT"); + static const struct auxiliary_device_id pmt_features_id_table[] = { { .name = "intel_vsec.discovery" }, {} diff --git a/drivers/platform/x86/intel/pmt/telemetry.c b/drivers/platform/x86/intel/pmt/telemetry.c index ac3a9bdf5601..58d06749e417 100644 --- a/drivers/platform/x86/intel/pmt/telemetry.c +++ b/drivers/platform/x86/intel/pmt/telemetry.c @@ -9,11 +9,14 @@ */ #include +#include #include #include +#include #include #include #include +#include #include #include @@ -311,6 +314,8 @@ static int pmt_telem_probe(struct auxiliary_device *auxdev, const struct auxilia continue; priv->num_entries++; + + intel_pmt_get_features(entry); } return 0; diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index 4bd0c6e7857c..f185e9c01c90 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -4,6 +4,7 @@ #include #include +#include /* * VSEC_CAP_UNUSED is reserved. It exists to prevent zero initialized @@ -166,6 +167,21 @@ struct oobmsm_plat_info { u8 function_number; }; +struct telemetry_region { + struct oobmsm_plat_info plat_info; + void __iomem *addr; + size_t size; + u32 guid; + u32 num_rmids; +}; + +struct pmt_feature_group { + enum pmt_feature_id id; + int count; + struct kref kref; + struct telemetry_region regions[]; +}; + int intel_vsec_add_aux(struct pci_dev *pdev, struct device *parent, struct intel_vsec_device *intel_vsec_dev, const char *name); -- cgit v1.2.3 From 42dabe5442887946b16e64c6ebe91d2671a96fbb Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:29 -0700 Subject: platform/x86/intel/pmt/telemetry: Add API to retrieve telemetry regions by feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new API, intel_pmt_get_regions_by_feature(), that gathers telemetry regions based on a provided capability flag. This API enables retrieval of regions with various capabilities (for example, RMID-based telemetry) and provides a unified interface for accessing them. Resource management is handled via reference counting using intel_pmt_put_feature_group(). Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-15-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/telemetry.c | 89 +++++++++++++++++++++++++++++- include/linux/intel_vsec.h | 18 ++++++ 2 files changed, 106 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/telemetry.c b/drivers/platform/x86/intel/pmt/telemetry.c index 58d06749e417..a4dfca6cac19 100644 --- a/drivers/platform/x86/intel/pmt/telemetry.c +++ b/drivers/platform/x86/intel/pmt/telemetry.c @@ -9,16 +9,21 @@ */ #include +#include +#include +#include #include #include #include #include #include +#include +#include #include #include #include #include -#include +#include #include "class.h" @@ -209,6 +214,87 @@ unlock: } EXPORT_SYMBOL_NS_GPL(pmt_telem_get_endpoint_info, "INTEL_PMT_TELEMETRY"); +static int pmt_copy_region(struct telemetry_region *region, + struct intel_pmt_entry *entry) +{ + + struct oobmsm_plat_info *plat_info; + + plat_info = intel_vsec_get_mapping(entry->ep->pcidev); + if (IS_ERR(plat_info)) + return PTR_ERR(plat_info); + + region->plat_info = *plat_info; + region->guid = entry->guid; + region->addr = entry->ep->base; + region->size = entry->size; + region->num_rmids = entry->num_rmids; + + return 0; +} + +static void pmt_feature_group_release(struct kref *kref) +{ + struct pmt_feature_group *feature_group; + + feature_group = container_of(kref, struct pmt_feature_group, kref); + kfree(feature_group); +} + +struct pmt_feature_group *intel_pmt_get_regions_by_feature(enum pmt_feature_id id) +{ + struct pmt_feature_group *feature_group __free(kfree) = NULL; + struct telemetry_region *region; + struct intel_pmt_entry *entry; + unsigned long idx; + int count = 0; + size_t size; + + if (!pmt_feature_id_is_valid(id)) + return ERR_PTR(-EINVAL); + + guard(mutex)(&ep_lock); + xa_for_each(&telem_array, idx, entry) { + if (entry->feature_flags & BIT(id)) + count++; + } + + if (!count) + return ERR_PTR(-ENOENT); + + size = struct_size(feature_group, regions, count); + feature_group = kzalloc(size, GFP_KERNEL); + if (!feature_group) + return ERR_PTR(-ENOMEM); + + feature_group->count = count; + + region = feature_group->regions; + xa_for_each(&telem_array, idx, entry) { + int ret; + + if (!(entry->feature_flags & BIT(id))) + continue; + + ret = pmt_copy_region(region, entry); + if (ret) + return ERR_PTR(ret); + + region++; + } + + kref_init(&feature_group->kref); + + return no_free_ptr(feature_group); +} +EXPORT_SYMBOL(intel_pmt_get_regions_by_feature); + +void intel_pmt_put_feature_group(struct pmt_feature_group *feature_group) +{ + kref_put(&feature_group->kref, pmt_feature_group_release); +} +EXPORT_SYMBOL(intel_pmt_put_feature_group); + int pmt_telem_read(struct telem_endpoint *ep, u32 id, u64 *data, u32 count) { u32 offset, size; @@ -353,3 +439,4 @@ MODULE_AUTHOR("David E. Box "); MODULE_DESCRIPTION("Intel PMT Telemetry driver"); MODULE_LICENSE("GPL v2"); MODULE_IMPORT_NS("INTEL_PMT"); +MODULE_IMPORT_NS("INTEL_VSEC"); diff --git a/include/linux/intel_vsec.h b/include/linux/intel_vsec.h index f185e9c01c90..53f6fe88e369 100644 --- a/include/linux/intel_vsec.h +++ b/include/linux/intel_vsec.h @@ -4,6 +4,7 @@ #include #include +#include #include /* @@ -218,4 +219,21 @@ static inline struct oobmsm_plat_info *intel_vsec_get_mapping(struct pci_dev *pd return ERR_PTR(-ENODEV); } #endif + +#if IS_ENABLED(CONFIG_INTEL_PMT_TELEMETRY) +struct pmt_feature_group * +intel_pmt_get_regions_by_feature(enum pmt_feature_id id); + +void intel_pmt_put_feature_group(struct pmt_feature_group *feature_group); +#else +static inline struct pmt_feature_group * +intel_pmt_get_regions_by_feature(enum pmt_feature_id id) +{ + return ERR_PTR(-ENODEV); +} + +static inline void +intel_pmt_put_feature_group(struct pmt_feature_group *feature_group) {} +#endif + #endif -- cgit v1.2.3 From b9707d46a95962bb4e28ae1929015e419ad6aff7 Mon Sep 17 00:00:00 2001 From: "David E. Box" Date: Wed, 2 Jul 2025 19:28:30 -0700 Subject: platform/x86/intel/pmt: KUNIT test for PMT Enhanced Discovery API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a KUNIT test for the intel_pmt_get_regions_by_feature() API. Signed-off-by: David E. Box Link: https://lore.kernel.org/r/20250703022832.1302928-16-david.e.box@linux.intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/Kconfig | 14 +++ drivers/platform/x86/intel/pmt/Makefile | 2 + drivers/platform/x86/intel/pmt/discovery-kunit.c | 116 +++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 drivers/platform/x86/intel/pmt/discovery-kunit.c (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/Kconfig b/drivers/platform/x86/intel/pmt/Kconfig index 83ae17eab462..785c206e1beb 100644 --- a/drivers/platform/x86/intel/pmt/Kconfig +++ b/drivers/platform/x86/intel/pmt/Kconfig @@ -51,3 +51,17 @@ config INTEL_PMT_DISCOVERY To compile this driver as a module, choose M here: the module will be called pmt_discovery. + +config INTEL_PMT_KUNIT_TEST + tristate "KUnit tests for Intel PMT driver" + depends on INTEL_PMT_DISCOVERY + depends on KUNIT + help + Enable this option to compile and run a suite of KUnit tests for the Intel + Platform Monitoring Technology (PMT) driver. These tests are designed to + validate the driver's functionality, error handling, and overall stability, + helping developers catch regressions and ensure code quality during changes. + + This option is intended for development and testing environments. It is + recommended to disable it in production builds. To compile this driver as a + module, choose M here: the module will be called pmt-discovery-kunit. diff --git a/drivers/platform/x86/intel/pmt/Makefile b/drivers/platform/x86/intel/pmt/Makefile index 8aed7e1592e4..47f692c091c9 100644 --- a/drivers/platform/x86/intel/pmt/Makefile +++ b/drivers/platform/x86/intel/pmt/Makefile @@ -12,3 +12,5 @@ obj-$(CONFIG_INTEL_PMT_CRASHLOG) += pmt_crashlog.o pmt_crashlog-y := crashlog.o obj-$(CONFIG_INTEL_PMT_DISCOVERY) += pmt_discovery.o pmt_discovery-y := discovery.o features.o +obj-$(CONFIG_INTEL_PMT_KUNIT_TEST) += pmt-discovery-kunit.o +pmt-discovery-kunit-y := discovery-kunit.o diff --git a/drivers/platform/x86/intel/pmt/discovery-kunit.c b/drivers/platform/x86/intel/pmt/discovery-kunit.c new file mode 100644 index 000000000000..b4493fb96738 --- /dev/null +++ b/drivers/platform/x86/intel/pmt/discovery-kunit.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Intel Platform Monitory Technology Discovery KUNIT tests + * + * Copyright (c) 2025, Intel Corporation. + * All Rights Reserved. + */ + +#include +#include +#include +#include +#include +#include + +#define PMT_FEATURE_COUNT (FEATURE_MAX + 1) + +static void +validate_pmt_regions(struct kunit *test, struct pmt_feature_group *feature_group, int feature_id) +{ + int i; + + kunit_info(test, "Feature ID %d [%s] has %d regions.\n", feature_id, + pmt_feature_names[feature_id], feature_group->count); + + for (i = 0; i < feature_group->count; i++) { + struct telemetry_region *region = &feature_group->regions[i]; + + kunit_info(test, " - Region %d: cdie_mask=%u, package_id=%u, partition=%u, segment=%u,", + i, region->plat_info.cdie_mask, region->plat_info.package_id, + region->plat_info.partition, region->plat_info.segment); + kunit_info(test, "\t\tbus=%u, device=%u, function=%u, guid=0x%x,", + region->plat_info.bus_number, region->plat_info.device_number, + region->plat_info.function_number, region->guid); + kunit_info(test, "\t\taddr=%p, size=%lu, num_rmids=%u", region->addr, region->size, + region->num_rmids); + + + KUNIT_ASSERT_GE(test, region->plat_info.cdie_mask, 0); + KUNIT_ASSERT_GE(test, region->plat_info.package_id, 0); + KUNIT_ASSERT_GE(test, region->plat_info.partition, 0); + KUNIT_ASSERT_GE(test, region->plat_info.segment, 0); + KUNIT_ASSERT_GE(test, region->plat_info.bus_number, 0); + KUNIT_ASSERT_GE(test, region->plat_info.device_number, 0); + KUNIT_ASSERT_GE(test, region->plat_info.function_number, 0); + + KUNIT_ASSERT_NE(test, region->guid, 0); + + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, (__force const void *)region->addr); + } +} + +static void linebreak(struct kunit *test) +{ + kunit_info(test, "*****************************************************************************\n"); +} + +static void test_intel_pmt_get_regions_by_feature(struct kunit *test) +{ + struct pmt_feature_group *feature_group; + int num_available = 0; + int feature_id; + + /* Iterate through all possible feature IDs */ + for (feature_id = 1; feature_id < PMT_FEATURE_COUNT; feature_id++, linebreak(test)) { + const char *name; + + if (!pmt_feature_id_is_valid(feature_id)) + continue; + + name = pmt_feature_names[feature_id]; + + feature_group = intel_pmt_get_regions_by_feature(feature_id); + if (IS_ERR(feature_group)) { + if (PTR_ERR(feature_group) == -ENOENT) + kunit_warn(test, "intel_pmt_get_regions_by_feature() reporting feature %d [%s] is not present.\n", + feature_id, name); + else + kunit_warn(test, "intel_pmt_get_regions_by_feature() returned error %ld while attempt to lookup %d [%s].\n", + PTR_ERR(feature_group), feature_id, name); + + continue; + } + + if (!feature_group) { + kunit_warn(test, "Feature ID %d: %s is not available.\n", feature_id, name); + continue; + } + + num_available++; + + validate_pmt_regions(test, feature_group, feature_id); + + intel_pmt_put_feature_group(feature_group); + } + + if (num_available == 0) + kunit_warn(test, "No PMT region groups were available for any feature ID (0-10).\n"); +} + +static struct kunit_case intel_pmt_discovery_test_cases[] = { + KUNIT_CASE(test_intel_pmt_get_regions_by_feature), + {} +}; + +static struct kunit_suite intel_pmt_discovery_test_suite = { + .name = "pmt_discovery_test", + .test_cases = intel_pmt_discovery_test_cases, +}; + +kunit_test_suite(intel_pmt_discovery_test_suite); + +MODULE_IMPORT_NS("INTEL_PMT_DISCOVERY"); +MODULE_AUTHOR("David E. Box "); +MODULE_DESCRIPTION("Intel PMT Discovery KUNIT test driver"); +MODULE_LICENSE("GPL"); -- cgit v1.2.3 From 56036d6af41a473a8129fc960a5ab3673eda13d5 Mon Sep 17 00:00:00 2001 From: Stuart Hayes Date: Mon, 9 Jun 2025 13:46:57 -0500 Subject: platform/x86: dell_rbu: Remove unused struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop using an entire struct packet_data just for its embedded list_head. Signed-off-by: Stuart Hayes Link: https://lore.kernel.org/r/20250609184659.7210-4-stuart.w.hayes@gmail.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/dell/dell_rbu.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/dell/dell_rbu.c b/drivers/platform/x86/dell/dell_rbu.c index 9dd9f2cb074f..45c0a72e494a 100644 --- a/drivers/platform/x86/dell/dell_rbu.c +++ b/drivers/platform/x86/dell/dell_rbu.c @@ -77,14 +77,14 @@ struct packet_data { int ordernum; }; -static struct packet_data packet_data_head; +static struct list_head packet_data_list; static struct platform_device *rbu_device; static int context; static void init_packet_head(void) { - INIT_LIST_HEAD(&packet_data_head.list); + INIT_LIST_HEAD(&packet_data_list); rbu_data.packet_read_count = 0; rbu_data.num_packets = 0; rbu_data.packetsize = 0; @@ -183,7 +183,7 @@ static int create_packet(void *data, size_t length) __must_hold(&rbu_data.lock) /* initialize the newly created packet headers */ INIT_LIST_HEAD(&newpacket->list); - list_add_tail(&newpacket->list, &packet_data_head.list); + list_add_tail(&newpacket->list, &packet_data_list); memcpy(newpacket->data, data, length); @@ -292,7 +292,7 @@ static int packet_read_list(char *data, size_t * pread_length) remaining_bytes = *pread_length; bytes_read = rbu_data.packet_read_count; - list_for_each_entry(newpacket, &packet_data_head.list, list) { + list_for_each_entry(newpacket, &packet_data_list, list) { bytes_copied = do_packet_read(pdest, newpacket, remaining_bytes, bytes_read, &temp_count); remaining_bytes -= bytes_copied; @@ -315,7 +315,7 @@ static void packet_empty_list(void) { struct packet_data *newpacket, *tmp; - list_for_each_entry_safe(newpacket, tmp, &packet_data_head.list, list) { + list_for_each_entry_safe(newpacket, tmp, &packet_data_list, list) { list_del(&newpacket->list); /* -- cgit v1.2.3 From 428f6f3a56ac85f37a07a3fe5149b593185d5c4c Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 8 Jul 2025 16:41:01 -0700 Subject: platform/x86/intel/pmt/discovery: Fix size_t specifiers for 32-bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When building i386 allmodconfig, there are two warnings in the newly added discovery code: drivers/platform/x86/intel/pmt/discovery.c: In function 'pmt_feature_get_feature_table': drivers/platform/x86/intel/pmt/discovery.c:427:35: error: format '%ld' expects argument of type 'long int', but argument 2 has type 'size_t' {aka 'unsigned int'} [-Werror=format=] 427 | if (WARN(size > res_size, "Bad table size %ld > %pa", size, &res_size)) | ^~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~ | | | size_t {aka unsigned int} ... drivers/platform/x86/intel/pmt/discovery.c:427:53: note: format string is defined here 427 | if (WARN(size > res_size, "Bad table size %ld > %pa", size, &res_size)) | ~~^ | | | long int | %d drivers/platform/x86/intel/pmt/discovery-kunit.c: In function 'validate_pmt_regions': include/linux/kern_levels.h:5:25: error: format '%lu' expects argument of type 'long unsigned int', but argument 4 has type 'size_t' {aka 'unsigned int'} [-Werror=format=] ... drivers/platform/x86/intel/pmt/discovery-kunit.c:35:17: note: in expansion of macro 'kunit_info' 35 | kunit_info(test, "\t\taddr=%p, size=%lu, num_rmids=%u", region->addr, region->size, | ^~~~~~~~~~ size_t is 'unsigned long' for 64-bit platforms but 'unsigned int' for 32-bit platforms, so '%ld' is not correct. Use the proper size_t specifier, '%zu', to resolve the warnings on 32-bit platforms while not affecting 64-bit platforms. Reported-by: Randy Dunlap Reported-by: kernelci.org bot Fixes: d9a078809356 ("platform/x86/intel/pmt: Add PMT Discovery driver") Fixes: b9707d46a959 ("platform/x86/intel/pmt: KUNIT test for PMT Enhanced Discovery API") Closes: https://lore.kernel.org/all/CACo-S-29Degjym-azsJNSd1yofLOB2_Rf5xpa9b7L-14OPn7wQ@mail.gmail.com/ Signed-off-by: Nathan Chancellor Link: https://lore.kernel.org/r/20250708-discovery-pmt-fix-32-bit-formats-v1-1-296a5fc9c3d4@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/discovery-kunit.c | 2 +- drivers/platform/x86/intel/pmt/discovery.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/discovery-kunit.c b/drivers/platform/x86/intel/pmt/discovery-kunit.c index b4493fb96738..f44eb41d58f6 100644 --- a/drivers/platform/x86/intel/pmt/discovery-kunit.c +++ b/drivers/platform/x86/intel/pmt/discovery-kunit.c @@ -32,7 +32,7 @@ validate_pmt_regions(struct kunit *test, struct pmt_feature_group *feature_group kunit_info(test, "\t\tbus=%u, device=%u, function=%u, guid=0x%x,", region->plat_info.bus_number, region->plat_info.device_number, region->plat_info.function_number, region->guid); - kunit_info(test, "\t\taddr=%p, size=%lu, num_rmids=%u", region->addr, region->size, + kunit_info(test, "\t\taddr=%p, size=%zu, num_rmids=%u", region->addr, region->size, region->num_rmids); diff --git a/drivers/platform/x86/intel/pmt/discovery.c b/drivers/platform/x86/intel/pmt/discovery.c index e72d43b675b4..1a680a042a98 100644 --- a/drivers/platform/x86/intel/pmt/discovery.c +++ b/drivers/platform/x86/intel/pmt/discovery.c @@ -424,7 +424,7 @@ pmt_feature_get_feature_table(struct pmt_features_priv *priv, size = sizeof(*header) + FEAT_ATTR_SIZE(header->attr_size) + PMT_GUID_SIZE(header->num_guids); res_size = resource_size(&res); - if (WARN(size > res_size, "Bad table size %ld > %pa", size, &res_size)) + if (WARN(size > res_size, "Bad table size %zu > %pa", size, &res_size)) return -EINVAL; /* Get the feature attributes, including capability fields */ -- cgit v1.2.3 From bde430fb669d03a0025fd90485dc26acfafd9b4f Mon Sep 17 00:00:00 2001 From: Suma Hegde Date: Wed, 9 Jul 2025 10:54:13 +0000 Subject: platform/x86/amd/hsmp: Enhance the print messages to prevent confusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the HSMP ACPI device is present, loading the amd_hsmp.ko module incorrectly displays the message "HSMP is not supported on Family:%x model:%x\n" despite being supported by the hsmp_acpi.ko module, leading to confusion. To address this issue, relocate the acpi_dev_present() check to the beginning of the hsmp_plt_init() and revise the print message to better reflect the current support status. Additionally, add more error messages in the error paths and debug messages to indicate successful probing for both hsmp_acpi.ko and amd_hsmp.ko modules. Reviewed-by: Naveen Krishna Chatradhi Signed-off-by: Suma Hegde Link: https://lore.kernel.org/r/20250709105413.2487851-1-suma.hegde@amd.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/amd/hsmp/acpi.c | 9 +++++++-- drivers/platform/x86/amd/hsmp/plat.c | 28 ++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/amd/hsmp/acpi.c b/drivers/platform/x86/amd/hsmp/acpi.c index 2f1faa82d13e..d974c2289f5a 100644 --- a/drivers/platform/x86/amd/hsmp/acpi.c +++ b/drivers/platform/x86/amd/hsmp/acpi.c @@ -587,8 +587,10 @@ static int hsmp_acpi_probe(struct platform_device *pdev) if (!hsmp_pdev->is_probed) { hsmp_pdev->num_sockets = amd_num_nodes(); - if (hsmp_pdev->num_sockets == 0 || hsmp_pdev->num_sockets > MAX_AMD_NUM_NODES) + if (hsmp_pdev->num_sockets == 0 || hsmp_pdev->num_sockets > MAX_AMD_NUM_NODES) { + dev_err(&pdev->dev, "Wrong number of sockets\n"); return -ENODEV; + } hsmp_pdev->sock = devm_kcalloc(&pdev->dev, hsmp_pdev->num_sockets, sizeof(*hsmp_pdev->sock), @@ -605,9 +607,12 @@ static int hsmp_acpi_probe(struct platform_device *pdev) if (!hsmp_pdev->is_probed) { ret = hsmp_misc_register(&pdev->dev); - if (ret) + if (ret) { + dev_err(&pdev->dev, "Failed to register misc device\n"); return ret; + } hsmp_pdev->is_probed = true; + dev_dbg(&pdev->dev, "AMD HSMP ACPI is probed successfully\n"); } return 0; diff --git a/drivers/platform/x86/amd/hsmp/plat.c b/drivers/platform/x86/amd/hsmp/plat.c index e3874c47ed9e..f8aa844d33e4 100644 --- a/drivers/platform/x86/amd/hsmp/plat.c +++ b/drivers/platform/x86/amd/hsmp/plat.c @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -215,7 +217,14 @@ static int hsmp_pltdrv_probe(struct platform_device *pdev) return ret; } - return hsmp_misc_register(&pdev->dev); + ret = hsmp_misc_register(&pdev->dev); + if (ret) { + dev_err(&pdev->dev, "Failed to register misc device\n"); + return ret; + } + + dev_dbg(&pdev->dev, "AMD HSMP is probed successfully\n"); + return 0; } static void hsmp_pltdrv_remove(struct platform_device *pdev) @@ -287,15 +296,20 @@ static int __init hsmp_plt_init(void) { int ret = -ENODEV; + if (acpi_dev_present(ACPI_HSMP_DEVICE_HID, NULL, -1)) { + if (IS_ENABLED(CONFIG_AMD_HSMP_ACPI)) + pr_debug("HSMP is supported through ACPI on this platform, please use hsmp_acpi.ko\n"); + else + pr_info("HSMP is supported through ACPI on this platform, please enable AMD_HSMP_ACPI config\n"); + return -ENODEV; + } + if (!legacy_hsmp_support()) { - pr_info("HSMP is not supported on Family:%x model:%x\n", + pr_info("HSMP interface is either disabled or not supported on family:%x model:%x\n", boot_cpu_data.x86, boot_cpu_data.x86_model); return ret; } - if (acpi_dev_present(ACPI_HSMP_DEVICE_HID, NULL, -1)) - return -ENODEV; - hsmp_pdev = get_hsmp_pdev(); if (!hsmp_pdev) return -ENOMEM; @@ -305,8 +319,10 @@ static int __init hsmp_plt_init(void) * if we have N SMN/DF interfaces that ideally means N sockets */ hsmp_pdev->num_sockets = amd_num_nodes(); - if (hsmp_pdev->num_sockets == 0 || hsmp_pdev->num_sockets > MAX_AMD_NUM_NODES) + if (hsmp_pdev->num_sockets == 0 || hsmp_pdev->num_sockets > MAX_AMD_NUM_NODES) { + pr_err("Wrong number of sockets\n"); return ret; + } ret = platform_driver_register(&amd_hsmp_driver); if (ret) -- cgit v1.2.3 From 6382c27389c266e90b31151a79d81fa6e122d2d6 Mon Sep 17 00:00:00 2001 From: Arnd Bergmann Date: Fri, 11 Jul 2025 09:27:09 +0200 Subject: platform/x86/intel/pmt/discovery: fix format string warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When -Wformat-security is enabled, this new code triggers it: drivers/platform/x86/intel/pmt/discovery.c: In function 'pmt_features_discovery': drivers/platform/x86/intel/pmt/discovery.c:505:36: error: format not a string literal and no format arguments [-Werror=format-security] 505 | pmt_feature_names[feature->id]); Fixes: d9a078809356 ("platform/x86/intel/pmt: Add PMT Discovery driver") Signed-off-by: Arnd Bergmann Link: https://lore.kernel.org/r/20250711072718.2748415-1-arnd@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/discovery.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/discovery.c b/drivers/platform/x86/intel/pmt/discovery.c index 1a680a042a98..32713a194a55 100644 --- a/drivers/platform/x86/intel/pmt/discovery.c +++ b/drivers/platform/x86/intel/pmt/discovery.c @@ -502,7 +502,7 @@ static int pmt_features_discovery(struct pmt_features_priv *priv, } ret = kobject_init_and_add(&feature->kobj, ktype, &priv->dev->kobj, - pmt_feature_names[feature->id]); + "%s", pmt_feature_names[feature->id]); if (ret) return ret; -- cgit v1.2.3 From 6e38b9fcbfa3053e1b5d2806a7233078d712bd34 Mon Sep 17 00:00:00 2001 From: Arnd Bergmann Date: Wed, 9 Jul 2025 17:17:28 +0200 Subject: platform/x86: lenovo: gamezone needs "other mode" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering the "other mode" notifier fails if that is disabled: x86_64-linux-ld: drivers/platform/x86/lenovo/wmi-gamezone.o: in function `lwmi_gz_probe': wmi-gamezone.c:(.text+0x336): undefined reference to `devm_lwmi_om_register_notifier' This could be fixed by adding a stub helper, but a Kconfig 'select' seems simpler here. Fixes: 22024ac5366f ("platform/x86: Add Lenovo Gamezone WMI Driver") Signed-off-by: Arnd Bergmann Link: https://lore.kernel.org/r/20250709151734.1268435-1-arnd@kernel.org [ij: retained the other selects as wmi-gamezone is using them directly.] Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/lenovo/Kconfig | 1 + 1 file changed, 1 insertion(+) (limited to 'drivers') diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index b76157b35296..d22b774e0236 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -252,6 +252,7 @@ config LENOVO_WMI_GAMEZONE select ACPI_PLATFORM_PROFILE select LENOVO_WMI_EVENTS select LENOVO_WMI_HELPERS + select LENOVO_WMI_TUNING 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. -- cgit v1.2.3 From 5a9fffd8a533bfb2688ec69dd6d1b6e53ef1177a Mon Sep 17 00:00:00 2001 From: Arnd Bergmann Date: Mon, 14 Jul 2025 10:15:43 +0200 Subject: platform/x86/intel/pmt: fix build dependency for kunit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When INTEL_PMT_TELEMETRY is in a loadable module, the discovery test cannot be built-in: x86_64-linux-ld: drivers/platform/x86/intel/pmt/discovery-kunit.o: in function `test_intel_pmt_get_regions_by_feature': discovery-kunit.c:(.text+0x29d): undefined reference to `intel_pmt_get_regions_by_feature' x86_64-linux-ld: discovery-kunit.c:(.text+0x2c3): undefined reference to `intel_pmt_put_feature_group' Add a Kconfig dependency to prevent this. Fixes: b9707d46a959 ("platform/x86/intel/pmt: KUNIT test for PMT Enhanced Discovery API") Signed-off-by: Arnd Bergmann Link: https://lore.kernel.org/r/20250714081559.4056777-1-arnd@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/Kconfig | 1 + 1 file changed, 1 insertion(+) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/Kconfig b/drivers/platform/x86/intel/pmt/Kconfig index 785c206e1beb..7363446b7773 100644 --- a/drivers/platform/x86/intel/pmt/Kconfig +++ b/drivers/platform/x86/intel/pmt/Kconfig @@ -55,6 +55,7 @@ config INTEL_PMT_DISCOVERY config INTEL_PMT_KUNIT_TEST tristate "KUnit tests for Intel PMT driver" depends on INTEL_PMT_DISCOVERY + depends on INTEL_PMT_TELEMETRY || !INTEL_PMT_TELEMETRY depends on KUNIT help Enable this option to compile and run a suite of KUnit tests for the Intel -- cgit v1.2.3 From cf685b3826e62a5e8bdc61135c0a60d128673e1b Mon Sep 17 00:00:00 2001 From: Thomas Zimmermann Date: Tue, 15 Jul 2025 14:24:38 +0200 Subject: platform/x86: dell-uart-backlight: Use blacklight power constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backlight subsystem has gotten its own power constants. Replace FB_BLANK_UNBLANK with BACKLIGHT_POWER_ON. Signed-off-by: Thomas Zimmermann Reviewed-by: Hans de Goede Link: https://lore.kernel.org/r/20250715122643.137027-2-tzimmermann@suse.de Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/dell/dell-uart-backlight.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/dell/dell-uart-backlight.c b/drivers/platform/x86/dell/dell-uart-backlight.c index 8f868f845350..f323a667dc2d 100644 --- a/drivers/platform/x86/dell/dell-uart-backlight.c +++ b/drivers/platform/x86/dell/dell-uart-backlight.c @@ -305,7 +305,7 @@ static int dell_uart_bl_serdev_probe(struct serdev_device *serdev) dev_dbg(dev, "Firmware version: %.*s\n", resp[RESP_LEN] - 3, resp + RESP_DATA); /* Initialize bl_power to a known value */ - ret = dell_uart_set_bl_power(dell_bl, FB_BLANK_UNBLANK); + ret = dell_uart_set_bl_power(dell_bl, BACKLIGHT_POWER_ON); if (ret) return ret; -- cgit v1.2.3 From 4ff3aeb664f7dfe824ba91ffb0b203397a8d431e Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Fri, 18 Jul 2025 12:23:05 -0500 Subject: platform/x86/amd: pmc: Add Lenovo Yoga 6 13ALC6 to pmc quirk list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lenovo Yoga 6 13ACL6 82ND has a similar BIOS problem as other Lenovo laptops from that vintage that causes a rather long resume from suspend. Add it to the quirk list that manipulates the scratch register to avoid the issue. Reported-by: Adam Berglund Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/4434 Tested-by: Adam Berglund Signed-off-by: Mario Limonciello Reviewed-by: Hans de Goede Link: https://lore.kernel.org/r/20250718172307.1928744-1-superm1@kernel.org Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/amd/pmc/pmc-quirks.c | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/amd/pmc/pmc-quirks.c b/drivers/platform/x86/amd/pmc/pmc-quirks.c index 131f10b68308..ded4c84f5ed1 100644 --- a/drivers/platform/x86/amd/pmc/pmc-quirks.c +++ b/drivers/platform/x86/amd/pmc/pmc-quirks.c @@ -190,6 +190,15 @@ static const struct dmi_system_id fwbug_list[] = { DMI_MATCH(DMI_PRODUCT_NAME, "82XQ"), } }, + /* https://gitlab.freedesktop.org/drm/amd/-/issues/4434 */ + { + .ident = "Lenovo Yoga 6 13ALC6", + .driver_data = &quirk_s2idle_bug, + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "82ND"), + } + }, /* https://gitlab.freedesktop.org/drm/amd/-/issues/2684 */ { .ident = "HP Laptop 15s-eq2xxx", -- cgit v1.2.3 From de2884c6cdd3d133704ce37393590dd1c761500c Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Wed, 2 Jul 2025 20:28:43 +0200 Subject: platform/x86: samsung-laptop: Expose charge_types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support the newly introduced charge_types sysfs attribute as a replacement for the custom `battery_life_extender` attribute. Setting charge_types to `Long Life` enables battery life extending mode. This change is similar to the recent Ideapad patch adding support for charge_types. Signed-off-by: Jelle van der Waa Link: https://lore.kernel.org/r/20250702182844.107706-1-jvanderwaa@redhat.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- .../ABI/obsolete/sysfs-driver-samsung-laptop | 10 ++ .../ABI/testing/sysfs-driver-samsung-laptop | 11 --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/samsung-laptop.c | 110 +++++++++++++++++++++ 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 Documentation/ABI/obsolete/sysfs-driver-samsung-laptop (limited to 'drivers') diff --git a/Documentation/ABI/obsolete/sysfs-driver-samsung-laptop b/Documentation/ABI/obsolete/sysfs-driver-samsung-laptop new file mode 100644 index 000000000000..204c3f3a1d78 --- /dev/null +++ b/Documentation/ABI/obsolete/sysfs-driver-samsung-laptop @@ -0,0 +1,10 @@ +What: /sys/devices/platform/samsung/battery_life_extender +Date: December 1, 2011 +KernelVersion: 3.3 +Contact: Corentin Chary +Description: Max battery charge level can be modified, battery cycle + life can be extended by reducing the max battery charge + level. + + - 0 means normal battery mode (100% charge) + - 1 means battery life extender mode (80% charge) diff --git a/Documentation/ABI/testing/sysfs-driver-samsung-laptop b/Documentation/ABI/testing/sysfs-driver-samsung-laptop index 28c9c040de5d..408cb0ddf4aa 100644 --- a/Documentation/ABI/testing/sysfs-driver-samsung-laptop +++ b/Documentation/ABI/testing/sysfs-driver-samsung-laptop @@ -20,17 +20,6 @@ Description: Some Samsung laptops have different "performance levels" and it's still unknown if this value even changes anything, other than making the user feel a bit better. -What: /sys/devices/platform/samsung/battery_life_extender -Date: December 1, 2011 -KernelVersion: 3.3 -Contact: Corentin Chary -Description: Max battery charge level can be modified, battery cycle - life can be extended by reducing the max battery charge - level. - - - 0 means normal battery mode (100% charge) - - 1 means battery life extender mode (80% charge) - What: /sys/devices/platform/samsung/usb_charge Date: December 1, 2011 KernelVersion: 3.3 diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 49ca98df47fd..6d238e120dce 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -619,6 +619,7 @@ config SAMSUNG_LAPTOP tristate "Samsung Laptop driver" depends on RFKILL || RFKILL = n depends on ACPI_VIDEO || ACPI_VIDEO = n + depends on ACPI_BATTERY depends on BACKLIGHT_CLASS_DEVICE select LEDS_CLASS select NEW_LEDS diff --git a/drivers/platform/x86/samsung-laptop.c b/drivers/platform/x86/samsung-laptop.c index decde4c9a3d9..9d43a12db73c 100644 --- a/drivers/platform/x86/samsung-laptop.c +++ b/drivers/platform/x86/samsung-laptop.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include /* @@ -348,6 +350,8 @@ struct samsung_laptop { struct notifier_block pm_nb; + struct acpi_battery_hook battery_hook; + bool handle_backlight; bool has_stepping_quirk; @@ -697,6 +701,11 @@ static ssize_t set_performance_level(struct device *dev, static DEVICE_ATTR(performance_level, 0644, get_performance_level, set_performance_level); +static void show_battery_life_extender_deprecation_warning(struct device *dev) +{ + dev_warn_once(dev, "battery_life_extender attribute has been deprecated, see charge_types.\n"); +} + static int read_battery_life_extender(struct samsung_laptop *samsung) { const struct sabi_commands *commands = &samsung->config->commands; @@ -739,6 +748,8 @@ static ssize_t get_battery_life_extender(struct device *dev, struct samsung_laptop *samsung = dev_get_drvdata(dev); int ret; + show_battery_life_extender_deprecation_warning(dev); + ret = read_battery_life_extender(samsung); if (ret < 0) return ret; @@ -753,6 +764,8 @@ static ssize_t set_battery_life_extender(struct device *dev, struct samsung_laptop *samsung = dev_get_drvdata(dev); int ret, value; + show_battery_life_extender_deprecation_warning(dev); + if (!count || kstrtoint(buf, 0, &value) != 0) return -EINVAL; @@ -766,6 +779,84 @@ static ssize_t set_battery_life_extender(struct device *dev, static DEVICE_ATTR(battery_life_extender, 0644, get_battery_life_extender, set_battery_life_extender); +static int samsung_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct samsung_laptop *samsung = ext_data; + + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + return write_battery_life_extender(samsung, 1); + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + return write_battery_life_extender(samsung, 0); + default: + return -EINVAL; + } +} + +static int samsung_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct samsung_laptop *samsung = ext_data; + int ret; + + ret = read_battery_life_extender(samsung); + if (ret < 0) + return ret; + + if (ret == 1) + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + + return 0; +} + +static int samsung_psy_prop_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp) +{ + return true; +} + +static const enum power_supply_property samsung_power_supply_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const struct power_supply_ext samsung_battery_ext = { + .name = "samsung_laptop", + .properties = samsung_power_supply_props, + .num_properties = ARRAY_SIZE(samsung_power_supply_props), + .charge_types = (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)), + .get_property = samsung_psy_ext_get_prop, + .set_property = samsung_psy_ext_set_prop, + .property_is_writeable = samsung_psy_prop_is_writeable, +}; + +static int samsung_battery_add(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct samsung_laptop *samsung = container_of(hook, struct samsung_laptop, battery_hook); + + return power_supply_register_extension(battery, &samsung_battery_ext, + &samsung->platform_device->dev, samsung); +} + +static int samsung_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &samsung_battery_ext); + + return 0; +} + static int read_usb_charge(struct samsung_laptop *samsung) { const struct sabi_commands *commands = &samsung->config->commands; @@ -1043,6 +1134,21 @@ static int __init samsung_lid_handling_init(struct samsung_laptop *samsung) return retval; } +static int __init samsung_battery_hook_init(struct samsung_laptop *samsung) +{ + int retval = 0; + + if (samsung->config->commands.get_battery_life_extender != 0xFFFF) { + samsung->battery_hook.add_battery = samsung_battery_add; + samsung->battery_hook.remove_battery = samsung_battery_remove; + samsung->battery_hook.name = "Samsung Battery Extension"; + retval = devm_battery_hook_register(&samsung->platform_device->dev, + &samsung->battery_hook); + } + + return retval; +} + static int kbd_backlight_enable(struct samsung_laptop *samsung) { const struct sabi_commands *commands = &samsung->config->commands; @@ -1604,6 +1710,10 @@ static int __init samsung_init(void) if (ret) goto error_lid_handling; + ret = samsung_battery_hook_init(samsung); + if (ret) + goto error_lid_handling; + samsung_debugfs_init(samsung); samsung->pm_nb.notifier_call = samsung_pm_notification; -- cgit v1.2.3 From 54d5cd4719c5e87f33d271c9ac2e393147d934f8 Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:31 -0400 Subject: platform/x86/intel/pmt: fix a crashlog NULL pointer access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usage of the intel_pmt_read() for binary sysfs, requires a pcidev. The current use of the endpoint value is only valid for telemetry endpoint usage. Without the ep, the crashlog usage causes the following NULL pointer exception: BUG: kernel NULL pointer dereference, address: 0000000000000000 Oops: Oops: 0000 [#1] SMP NOPTI RIP: 0010:intel_pmt_read+0x3b/0x70 [pmt_class] Code: Call Trace: ? sysfs_kf_bin_read+0xc0/0xe0 kernfs_fop_read_iter+0xac/0x1a0 vfs_read+0x26d/0x350 ksys_read+0x6b/0xe0 __x64_sys_read+0x1d/0x30 x64_sys_call+0x1bc8/0x1d70 do_syscall_64+0x6d/0x110 Augment struct intel_pmt_entry with a pointer to the pcidev to avoid the NULL pointer exception. Fixes: 045a513040cc ("platform/x86/intel/pmt: Use PMT callbacks") Cc: stable@vger.kernel.org Reviewed-by: David E. Box Reviewed-by: Tejas Upadhyay Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-2-michael.j.ruhl@intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/class.c | 3 ++- drivers/platform/x86/intel/pmt/class.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/class.c b/drivers/platform/x86/intel/pmt/class.c index a806a81ece52..5dd80109f591 100644 --- a/drivers/platform/x86/intel/pmt/class.c +++ b/drivers/platform/x86/intel/pmt/class.c @@ -99,7 +99,7 @@ intel_pmt_read(struct file *filp, struct kobject *kobj, if (count > entry->size - off) count = entry->size - off; - count = pmt_telem_read_mmio(entry->ep->pcidev, entry->cb, entry->header.guid, buf, + count = pmt_telem_read_mmio(entry->pcidev, entry->cb, entry->header.guid, buf, entry->base, off, count); return count; @@ -283,6 +283,7 @@ static int intel_pmt_populate_entry(struct intel_pmt_entry *entry, return -EINVAL; } + entry->pcidev = pci_dev; entry->guid = header->guid; entry->size = header->size; entry->cb = ivdev->priv_data; diff --git a/drivers/platform/x86/intel/pmt/class.h b/drivers/platform/x86/intel/pmt/class.h index fdf7e79d8c0d..ebd49e2cb2d5 100644 --- a/drivers/platform/x86/intel/pmt/class.h +++ b/drivers/platform/x86/intel/pmt/class.h @@ -40,6 +40,7 @@ struct intel_pmt_header { struct intel_pmt_entry { struct telem_endpoint *ep; + struct pci_dev *pcidev; struct intel_pmt_header header; struct bin_attribute pmt_bin_attr; struct kobject *kobj; -- cgit v1.2.3 From 0ba9e9cf76f2487654bc9bca38218780fa53030e Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:32 -0400 Subject: drm/xe: Correct the rev value for the DVSEC entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By definition, the Designated Vendor Specific Extended Capability (DVSEC) revision should be 1. Add the rev value to be correct. Fixes: 0c45e76fcc62 ("drm/xe/vsec: Support BMG devices") Signed-off-by: Michael J. Ruhl Reviewed-by: David E. Box Link: https://lore.kernel.org/r/20250713172943.7335-3-michael.j.ruhl@intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/gpu/drm/xe/xe_vsec.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'drivers') diff --git a/drivers/gpu/drm/xe/xe_vsec.c b/drivers/gpu/drm/xe/xe_vsec.c index b378848d3b7b..1bf7e709e110 100644 --- a/drivers/gpu/drm/xe/xe_vsec.c +++ b/drivers/gpu/drm/xe/xe_vsec.c @@ -24,6 +24,7 @@ #define BMG_DEVICE_ID 0xE2F8 static struct intel_vsec_header bmg_telemetry = { + .rev = 1, .length = 0x10, .id = VSEC_ID_TELEMETRY, .num_entries = 2, @@ -33,6 +34,7 @@ static struct intel_vsec_header bmg_telemetry = { }; static struct intel_vsec_header bmg_punit_crashlog = { + .rev = 1, .length = 0x10, .id = VSEC_ID_CRASHLOG, .num_entries = 1, @@ -42,6 +44,7 @@ static struct intel_vsec_header bmg_punit_crashlog = { }; static struct intel_vsec_header bmg_oobmsm_crashlog = { + .rev = 1, .length = 0x10, .id = VSEC_ID_CRASHLOG, .num_entries = 1, -- cgit v1.2.3 From 5b27388171a18cf6842c700520086ec50194e858 Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:33 -0400 Subject: drm/xe: Correct BMG VSEC header sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intel_vsec_header information for the crashlog feature is incorrect. Update the VSEC header with correct sizing and count. Since the crashlog entries are "merged" (num_entries = 2), the separate capabilities entries must be merged as well. Fixes: 0c45e76fcc62 ("drm/xe/vsec: Support BMG devices") Acked-by: Rodrigo Vivi Signed-off-by: Michael J. Ruhl Reviewed-by: David E. Box Link: https://lore.kernel.org/r/20250713172943.7335-4-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/gpu/drm/xe/xe_vsec.c | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) (limited to 'drivers') diff --git a/drivers/gpu/drm/xe/xe_vsec.c b/drivers/gpu/drm/xe/xe_vsec.c index 1bf7e709e110..56930ad42962 100644 --- a/drivers/gpu/drm/xe/xe_vsec.c +++ b/drivers/gpu/drm/xe/xe_vsec.c @@ -33,30 +33,19 @@ static struct intel_vsec_header bmg_telemetry = { .offset = BMG_DISCOVERY_OFFSET, }; -static struct intel_vsec_header bmg_punit_crashlog = { +static struct intel_vsec_header bmg_crashlog = { .rev = 1, .length = 0x10, .id = VSEC_ID_CRASHLOG, - .num_entries = 1, - .entry_size = 4, + .num_entries = 2, + .entry_size = 6, .tbir = 0, .offset = BMG_DISCOVERY_OFFSET + 0x60, }; -static struct intel_vsec_header bmg_oobmsm_crashlog = { - .rev = 1, - .length = 0x10, - .id = VSEC_ID_CRASHLOG, - .num_entries = 1, - .entry_size = 4, - .tbir = 0, - .offset = BMG_DISCOVERY_OFFSET + 0x78, -}; - static struct intel_vsec_header *bmg_capabilities[] = { &bmg_telemetry, - &bmg_punit_crashlog, - &bmg_oobmsm_crashlog, + &bmg_crashlog, NULL }; -- cgit v1.2.3 From ba22fe0cffedf5156731fbac729a638f18d17e6b Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:34 -0400 Subject: platform/x86/intel/pmt: white space cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Noticed two white space issues; cleaned them. Reviewed-by: David E. Box Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-5-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 6a9eb3c4b313..d40c8e212733 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -143,7 +143,7 @@ enable_show(struct device *dev, struct device_attribute *attr, char *buf) static ssize_t enable_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) + const char *buf, size_t count) { struct crashlog_entry *entry; bool enabled; @@ -177,7 +177,7 @@ trigger_show(struct device *dev, struct device_attribute *attr, char *buf) static ssize_t trigger_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) + const char *buf, size_t count) { struct crashlog_entry *entry; bool trigger; -- cgit v1.2.3 From 75a496aa054326d9ebf27b39e1af8b5b770311ba Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:35 -0400 Subject: platform/x86/intel/pmt: mutex clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header file for mutex usage and mutex_destroy() cleanup code is absent from the crashlog.c module. Add the header file and mutex_destroy(). Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-6-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index d40c8e212733..6e32fc1f8f1d 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -262,8 +263,12 @@ static void pmt_crashlog_remove(struct auxiliary_device *auxdev) struct pmt_crashlog_priv *priv = auxiliary_get_drvdata(auxdev); int i; - for (i = 0; i < priv->num_entries; i++) - intel_pmt_dev_destroy(&priv->entry[i].entry, &pmt_crashlog_ns); + for (i = 0; i < priv->num_entries; i++) { + struct crashlog_entry *crashlog = &priv->entry[i]; + + intel_pmt_dev_destroy(&crashlog->entry, &pmt_crashlog_ns); + mutex_destroy(&crashlog->control_mutex); + } } static int pmt_crashlog_probe(struct auxiliary_device *auxdev, -- cgit v1.2.3 From 4f8fa22d108006b8ec0b94bb67a1bd537a2bf141 Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:36 -0400 Subject: platform/x86/intel/pmt: use guard(mutex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the mutex paths to use the new guard() mechanism. With the removal of goto, do some minor cleanup of the current logic path. Reviewed-by: David E. Box Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-7-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 33 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 6e32fc1f8f1d..c3ca95854aba 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -156,9 +157,9 @@ enable_store(struct device *dev, struct device_attribute *attr, if (result) return result; - mutex_lock(&entry->control_mutex); + guard(mutex)(&entry->control_mutex); + pmt_crashlog_set_disable(&entry->entry, !enabled); - mutex_unlock(&entry->control_mutex); return count; } @@ -190,26 +191,24 @@ trigger_store(struct device *dev, struct device_attribute *attr, if (result) return result; - mutex_lock(&entry->control_mutex); + guard(mutex)(&entry->control_mutex); if (!trigger) { pmt_crashlog_set_clear(&entry->entry); - } else if (pmt_crashlog_complete(&entry->entry)) { - /* we cannot trigger a new crash if one is still pending */ - result = -EEXIST; - goto err; - } else if (pmt_crashlog_disabled(&entry->entry)) { - /* if device is currently disabled, return busy */ - result = -EBUSY; - goto err; - } else { - pmt_crashlog_set_execute(&entry->entry); + return count; } - result = count; -err: - mutex_unlock(&entry->control_mutex); - return result; + /* we cannot trigger a new crash if one is still pending */ + if (pmt_crashlog_complete(&entry->entry)) + return -EEXIST; + + /* if device is currently disabled, return busy */ + if (pmt_crashlog_disabled(&entry->entry)) + return -EBUSY; + + pmt_crashlog_set_execute(&entry->entry); + + return count; } static DEVICE_ATTR_RW(trigger); -- cgit v1.2.3 From 147c18d8efaa1fa006fdd0b901d51092dc3b2189 Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:37 -0400 Subject: platform/x86/intel/pmt: re-order trigger logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting the clear bit or checking the complete bit before checking to see if crashlog is disabled seems incorrect. Check disable before accessing any other bits. Reviewed-by: Ilpo Järvinen Reviewed-by: David E. Box Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-8-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index c3ca95854aba..440d2045e90d 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -193,6 +193,10 @@ trigger_store(struct device *dev, struct device_attribute *attr, guard(mutex)(&entry->control_mutex); + /* if device is currently disabled, return busy */ + if (pmt_crashlog_disabled(&entry->entry)) + return -EBUSY; + if (!trigger) { pmt_crashlog_set_clear(&entry->entry); return count; @@ -202,10 +206,6 @@ trigger_store(struct device *dev, struct device_attribute *attr, if (pmt_crashlog_complete(&entry->entry)) return -EEXIST; - /* if device is currently disabled, return busy */ - if (pmt_crashlog_disabled(&entry->entry)) - return -EBUSY; - pmt_crashlog_set_execute(&entry->entry); return count; -- cgit v1.2.3 From 5c7bfa1088274bc3820eb2083f8091ff8ad397ec Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:38 -0400 Subject: platform/x86/intel/pmt: correct types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A couple of local variables do not match the return types of some of the functions. Update the mismatched types to match. Reviewed-by: Ilpo Järvinen Reviewed-by: David E. Box Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-9-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 440d2045e90d..881f4abdae14 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -138,7 +138,7 @@ static ssize_t enable_show(struct device *dev, struct device_attribute *attr, char *buf) { struct intel_pmt_entry *entry = dev_get_drvdata(dev); - int enabled = !pmt_crashlog_disabled(entry); + bool enabled = !pmt_crashlog_disabled(entry); return sprintf(buf, "%d\n", enabled); } @@ -169,7 +169,7 @@ static ssize_t trigger_show(struct device *dev, struct device_attribute *attr, char *buf) { struct intel_pmt_entry *entry; - int trigger; + bool trigger; entry = dev_get_drvdata(dev); trigger = pmt_crashlog_complete(entry); -- cgit v1.2.3 From 8ab4f88d46c70bade0633b56a0f03e20102bf10c Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:39 -0400 Subject: platform/x86/intel/pmt: decouple sysfs and namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PMT namespace includes the crashlog sysfs attribute information. Other crashlog version/types may need different sysfs attributes. Coupling the attributes with the namespace blocks this usage. Decouple sysfs attributes from the name space and add them to the specific entry. Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-10-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/class.c | 12 ++++++------ drivers/platform/x86/intel/pmt/class.h | 2 +- drivers/platform/x86/intel/pmt/crashlog.c | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/class.c b/drivers/platform/x86/intel/pmt/class.c index 5dd80109f591..edcce340ea67 100644 --- a/drivers/platform/x86/intel/pmt/class.c +++ b/drivers/platform/x86/intel/pmt/class.c @@ -316,8 +316,8 @@ static int intel_pmt_dev_register(struct intel_pmt_entry *entry, entry->kobj = &dev->kobj; - if (ns->attr_grp) { - ret = sysfs_create_group(entry->kobj, ns->attr_grp); + if (entry->attr_grp) { + ret = sysfs_create_group(entry->kobj, entry->attr_grp); if (ret) goto fail_sysfs_create_group; } @@ -358,8 +358,8 @@ static int intel_pmt_dev_register(struct intel_pmt_entry *entry, fail_add_endpoint: sysfs_remove_bin_file(entry->kobj, &entry->pmt_bin_attr); fail_ioremap: - if (ns->attr_grp) - sysfs_remove_group(entry->kobj, ns->attr_grp); + if (entry->attr_grp) + sysfs_remove_group(entry->kobj, entry->attr_grp); fail_sysfs_create_group: device_unregister(dev); fail_dev_create: @@ -401,8 +401,8 @@ void intel_pmt_dev_destroy(struct intel_pmt_entry *entry, if (entry->size) sysfs_remove_bin_file(entry->kobj, &entry->pmt_bin_attr); - if (ns->attr_grp) - sysfs_remove_group(entry->kobj, ns->attr_grp); + if (entry->attr_grp) + sysfs_remove_group(entry->kobj, entry->attr_grp); device_unregister(dev); xa_erase(ns->xa, entry->devid); diff --git a/drivers/platform/x86/intel/pmt/class.h b/drivers/platform/x86/intel/pmt/class.h index ebd49e2cb2d5..3c5ad5f52bca 100644 --- a/drivers/platform/x86/intel/pmt/class.h +++ b/drivers/platform/x86/intel/pmt/class.h @@ -43,6 +43,7 @@ struct intel_pmt_entry { struct pci_dev *pcidev; struct intel_pmt_header header; struct bin_attribute pmt_bin_attr; + const struct attribute_group *attr_grp; struct kobject *kobj; void __iomem *disc_table; void __iomem *base; @@ -58,7 +59,6 @@ struct intel_pmt_entry { struct intel_pmt_namespace { const char *name; struct xarray *xa; - const struct attribute_group *attr_grp; int (*pmt_header_decode)(struct intel_pmt_entry *entry, struct device *dev); int (*pmt_add_endpoint)(struct intel_vsec_device *ivdev, diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 881f4abdae14..23b3971da40a 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -243,6 +243,8 @@ static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, /* Size is measured in DWORDS, but accessor returns bytes */ header->size = GET_SIZE(readl(disc_table + SIZE_OFFSET)); + entry->attr_grp = &pmt_crashlog_group; + return 0; } @@ -250,7 +252,6 @@ static DEFINE_XARRAY_ALLOC(crashlog_array); static struct intel_pmt_namespace pmt_crashlog_ns = { .name = "crashlog", .xa = &crashlog_array, - .attr_grp = &pmt_crashlog_group, .pmt_header_decode = pmt_crashlog_header_decode, }; -- cgit v1.2.3 From f57b32cb4adb28b62f61c4729f7b85f55518cb2b Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:40 -0400 Subject: platform/x86/intel/pmt: add register access helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The control register is used in a read/modify/write pattern. The status register is used in a read/check bit pattern. Add helpers to eliminate common code. Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-11-michael.j.ruhl@intel.com Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 23b3971da40a..ed781548e59d 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -64,20 +64,40 @@ struct pmt_crashlog_priv { /* * I/O */ -static bool pmt_crashlog_complete(struct intel_pmt_entry *entry) + +/* Read, modify, write the control register, setting or clearing @bit based on @set */ +static void pmt_crashlog_rmw(struct intel_pmt_entry *entry, u32 bit, bool set) { - u32 control = readl(entry->disc_table + CONTROL_OFFSET); + u32 reg = readl(entry->disc_table + CONTROL_OFFSET); + + reg &= ~CRASHLOG_FLAG_TRIGGER_MASK; + + if (set) + reg |= bit; + else + reg &= ~bit; + + writel(reg, entry->disc_table + CONTROL_OFFSET); +} + +/* Read the status register and see if the specified @bit is set */ +static bool pmt_crashlog_rc(struct intel_pmt_entry *entry, u32 bit) +{ + u32 reg = readl(entry->disc_table + CONTROL_OFFSET); + + return !!(reg & bit); +} +static bool pmt_crashlog_complete(struct intel_pmt_entry *entry) +{ /* return current value of the crashlog complete flag */ - return !!(control & CRASHLOG_FLAG_TRIGGER_COMPLETE); + return pmt_crashlog_rc(entry, CRASHLOG_FLAG_TRIGGER_COMPLETE); } static bool pmt_crashlog_disabled(struct intel_pmt_entry *entry) { - u32 control = readl(entry->disc_table + CONTROL_OFFSET); - /* return current value of the crashlog disabled flag */ - return !!(control & CRASHLOG_FLAG_DISABLE); + return pmt_crashlog_rc(entry, CRASHLOG_FLAG_DISABLE); } static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) @@ -98,37 +118,17 @@ static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) static void pmt_crashlog_set_disable(struct intel_pmt_entry *entry, bool disable) { - u32 control = readl(entry->disc_table + CONTROL_OFFSET); - - /* clear trigger bits so we are only modifying disable flag */ - control &= ~CRASHLOG_FLAG_TRIGGER_MASK; - - if (disable) - control |= CRASHLOG_FLAG_DISABLE; - else - control &= ~CRASHLOG_FLAG_DISABLE; - - writel(control, entry->disc_table + CONTROL_OFFSET); + pmt_crashlog_rmw(entry, CRASHLOG_FLAG_DISABLE, disable); } static void pmt_crashlog_set_clear(struct intel_pmt_entry *entry) { - u32 control = readl(entry->disc_table + CONTROL_OFFSET); - - control &= ~CRASHLOG_FLAG_TRIGGER_MASK; - control |= CRASHLOG_FLAG_TRIGGER_CLEAR; - - writel(control, entry->disc_table + CONTROL_OFFSET); + pmt_crashlog_rmw(entry, CRASHLOG_FLAG_TRIGGER_CLEAR, true); } static void pmt_crashlog_set_execute(struct intel_pmt_entry *entry) { - u32 control = readl(entry->disc_table + CONTROL_OFFSET); - - control &= ~CRASHLOG_FLAG_TRIGGER_MASK; - control |= CRASHLOG_FLAG_TRIGGER_EXECUTE; - - writel(control, entry->disc_table + CONTROL_OFFSET); + pmt_crashlog_rmw(entry, CRASHLOG_FLAG_TRIGGER_EXECUTE, true); } /* -- cgit v1.2.3 From 66df9fa783aadc2a5ae8ca11ead0b13032d24e7e Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:41 -0400 Subject: platform/x86/intel/pmt: refactor base parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To support an upcoming crashlog change, use the parent of struct intel_pmt_entry, struct crashlog_entry, as a main parameter. Using struct crashlog_entry will allow for a more flexible interface to control the crashlog feature. - Refactor to use struct crashlog_entry in place of struct intel_pmt_entry - Rename variables from "entry" to "crashlog" Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-12-michael.j.ruhl@intel.com Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 58 ++++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index ed781548e59d..087b7110ddd2 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -66,8 +66,9 @@ struct pmt_crashlog_priv { */ /* Read, modify, write the control register, setting or clearing @bit based on @set */ -static void pmt_crashlog_rmw(struct intel_pmt_entry *entry, u32 bit, bool set) +static void pmt_crashlog_rmw(struct crashlog_entry *crashlog, u32 bit, bool set) { + struct intel_pmt_entry *entry = &crashlog->entry; u32 reg = readl(entry->disc_table + CONTROL_OFFSET); reg &= ~CRASHLOG_FLAG_TRIGGER_MASK; @@ -81,23 +82,24 @@ static void pmt_crashlog_rmw(struct intel_pmt_entry *entry, u32 bit, bool set) } /* Read the status register and see if the specified @bit is set */ -static bool pmt_crashlog_rc(struct intel_pmt_entry *entry, u32 bit) +static bool pmt_crashlog_rc(struct crashlog_entry *crashlog, u32 bit) { + struct intel_pmt_entry *entry = &crashlog->entry; u32 reg = readl(entry->disc_table + CONTROL_OFFSET); return !!(reg & bit); } -static bool pmt_crashlog_complete(struct intel_pmt_entry *entry) +static bool pmt_crashlog_complete(struct crashlog_entry *crashlog) { /* return current value of the crashlog complete flag */ - return pmt_crashlog_rc(entry, CRASHLOG_FLAG_TRIGGER_COMPLETE); + return pmt_crashlog_rc(crashlog, CRASHLOG_FLAG_TRIGGER_COMPLETE); } -static bool pmt_crashlog_disabled(struct intel_pmt_entry *entry) +static bool pmt_crashlog_disabled(struct crashlog_entry *crashlog) { /* return current value of the crashlog disabled flag */ - return pmt_crashlog_rc(entry, CRASHLOG_FLAG_DISABLE); + return pmt_crashlog_rc(crashlog, CRASHLOG_FLAG_DISABLE); } static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) @@ -115,20 +117,20 @@ static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) return crash_type == CRASH_TYPE_OOBMSM && version == 0; } -static void pmt_crashlog_set_disable(struct intel_pmt_entry *entry, +static void pmt_crashlog_set_disable(struct crashlog_entry *crashlog, bool disable) { - pmt_crashlog_rmw(entry, CRASHLOG_FLAG_DISABLE, disable); + pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_DISABLE, disable); } -static void pmt_crashlog_set_clear(struct intel_pmt_entry *entry) +static void pmt_crashlog_set_clear(struct crashlog_entry *crashlog) { - pmt_crashlog_rmw(entry, CRASHLOG_FLAG_TRIGGER_CLEAR, true); + pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_TRIGGER_CLEAR, true); } -static void pmt_crashlog_set_execute(struct intel_pmt_entry *entry) +static void pmt_crashlog_set_execute(struct crashlog_entry *crashlog) { - pmt_crashlog_rmw(entry, CRASHLOG_FLAG_TRIGGER_EXECUTE, true); + pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_TRIGGER_EXECUTE, true); } /* @@ -137,8 +139,8 @@ static void pmt_crashlog_set_execute(struct intel_pmt_entry *entry) static ssize_t enable_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct intel_pmt_entry *entry = dev_get_drvdata(dev); - bool enabled = !pmt_crashlog_disabled(entry); + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool enabled = !pmt_crashlog_disabled(crashlog); return sprintf(buf, "%d\n", enabled); } @@ -147,19 +149,19 @@ static ssize_t enable_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct crashlog_entry *entry; + struct crashlog_entry *crashlog; bool enabled; int result; - entry = dev_get_drvdata(dev); + crashlog = dev_get_drvdata(dev); result = kstrtobool(buf, &enabled); if (result) return result; - guard(mutex)(&entry->control_mutex); + guard(mutex)(&crashlog->control_mutex); - pmt_crashlog_set_disable(&entry->entry, !enabled); + pmt_crashlog_set_disable(crashlog, !enabled); return count; } @@ -168,11 +170,11 @@ static DEVICE_ATTR_RW(enable); static ssize_t trigger_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct intel_pmt_entry *entry; + struct crashlog_entry *crashlog; bool trigger; - entry = dev_get_drvdata(dev); - trigger = pmt_crashlog_complete(entry); + crashlog = dev_get_drvdata(dev); + trigger = pmt_crashlog_complete(crashlog); return sprintf(buf, "%d\n", trigger); } @@ -181,32 +183,32 @@ static ssize_t trigger_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct crashlog_entry *entry; + struct crashlog_entry *crashlog; bool trigger; int result; - entry = dev_get_drvdata(dev); + crashlog = dev_get_drvdata(dev); result = kstrtobool(buf, &trigger); if (result) return result; - guard(mutex)(&entry->control_mutex); + guard(mutex)(&crashlog->control_mutex); /* if device is currently disabled, return busy */ - if (pmt_crashlog_disabled(&entry->entry)) + if (pmt_crashlog_disabled(crashlog)) return -EBUSY; if (!trigger) { - pmt_crashlog_set_clear(&entry->entry); + pmt_crashlog_set_clear(crashlog); return count; } /* we cannot trigger a new crash if one is still pending */ - if (pmt_crashlog_complete(&entry->entry)) + if (pmt_crashlog_complete(crashlog)) return -EEXIST; - pmt_crashlog_set_execute(&entry->entry); + pmt_crashlog_set_execute(crashlog); return count; } -- cgit v1.2.3 From 5623fa6859a6cd49366421317e3c5ab183583624 Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:42 -0400 Subject: platform/x86/intel/pmt: use a version struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In preparation for supporting multiple crashlog versions, use a struct to keep bit offset info for the status and control bits. Reviewed-by: Ilpo Järvinen Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-13-michael.j.ruhl@intel.com [ij: move crashlog_type1_ver0 to its final place & add consts to crashlog_info] Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 92 ++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 26 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index 087b7110ddd2..db54f881ba57 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -24,21 +24,6 @@ /* Crashlog discovery header types */ #define CRASH_TYPE_OOBMSM 1 -/* Control Flags */ -#define CRASHLOG_FLAG_DISABLE BIT(28) - -/* - * Bits 29 and 30 control the state of bit 31. - * - * Bit 29 will clear bit 31, if set, allowing a new crashlog to be captured. - * Bit 30 will immediately trigger a crashlog to be generated, setting bit 31. - * Bit 31 is the read-only status with a 1 indicating log is complete. - */ -#define CRASHLOG_FLAG_TRIGGER_CLEAR BIT(29) -#define CRASHLOG_FLAG_TRIGGER_EXECUTE BIT(30) -#define CRASHLOG_FLAG_TRIGGER_COMPLETE BIT(31) -#define CRASHLOG_FLAG_TRIGGER_MASK GENMASK(31, 28) - /* Crashlog Discovery Header */ #define CONTROL_OFFSET 0x0 #define GUID_OFFSET 0x4 @@ -50,10 +35,50 @@ /* size is in bytes */ #define GET_SIZE(v) ((v) * sizeof(u32)) +/* + * Type 1 Version 0 + * status and control registers are combined. + * + * Bits 29 and 30 control the state of bit 31. + * Bit 29 will clear bit 31, if set, allowing a new crashlog to be captured. + * Bit 30 will immediately trigger a crashlog to be generated, setting bit 31. + * Bit 31 is the read-only status with a 1 indicating log is complete. + */ +#define TYPE1_VER0_STATUS_OFFSET 0x00 +#define TYPE1_VER0_CONTROL_OFFSET 0x00 + +#define TYPE1_VER0_DISABLE BIT(28) +#define TYPE1_VER0_CLEAR BIT(29) +#define TYPE1_VER0_EXECUTE BIT(30) +#define TYPE1_VER0_COMPLETE BIT(31) +#define TYPE1_VER0_TRIGGER_MASK GENMASK(31, 28) + +/* After offset, order alphabetically, not bit ordered */ +struct crashlog_status { + u32 offset; + u32 cleared; + u32 complete; + u32 disabled; +}; + +struct crashlog_control { + u32 offset; + u32 trigger_mask; + u32 clear; + u32 disable; + u32 manual; +}; + +struct crashlog_info { + const struct crashlog_status status; + const struct crashlog_control control; +}; + struct crashlog_entry { /* entry must be first member of struct */ struct intel_pmt_entry entry; struct mutex control_mutex; + const struct crashlog_info *info; }; struct pmt_crashlog_priv { @@ -68,24 +93,25 @@ struct pmt_crashlog_priv { /* Read, modify, write the control register, setting or clearing @bit based on @set */ static void pmt_crashlog_rmw(struct crashlog_entry *crashlog, u32 bit, bool set) { + const struct crashlog_control *control = &crashlog->info->control; struct intel_pmt_entry *entry = &crashlog->entry; - u32 reg = readl(entry->disc_table + CONTROL_OFFSET); + u32 reg = readl(entry->disc_table + control->offset); - reg &= ~CRASHLOG_FLAG_TRIGGER_MASK; + reg &= ~control->trigger_mask; if (set) reg |= bit; else reg &= ~bit; - writel(reg, entry->disc_table + CONTROL_OFFSET); + writel(reg, entry->disc_table + control->offset); } /* Read the status register and see if the specified @bit is set */ static bool pmt_crashlog_rc(struct crashlog_entry *crashlog, u32 bit) { - struct intel_pmt_entry *entry = &crashlog->entry; - u32 reg = readl(entry->disc_table + CONTROL_OFFSET); + const struct crashlog_status *status = &crashlog->info->status; + u32 reg = readl(crashlog->entry.disc_table + status->offset); return !!(reg & bit); } @@ -93,13 +119,13 @@ static bool pmt_crashlog_rc(struct crashlog_entry *crashlog, u32 bit) static bool pmt_crashlog_complete(struct crashlog_entry *crashlog) { /* return current value of the crashlog complete flag */ - return pmt_crashlog_rc(crashlog, CRASHLOG_FLAG_TRIGGER_COMPLETE); + return pmt_crashlog_rc(crashlog, crashlog->info->status.complete); } static bool pmt_crashlog_disabled(struct crashlog_entry *crashlog) { /* return current value of the crashlog disabled flag */ - return pmt_crashlog_rc(crashlog, CRASHLOG_FLAG_DISABLE); + return pmt_crashlog_rc(crashlog, crashlog->info->status.disabled); } static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) @@ -120,17 +146,17 @@ static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) static void pmt_crashlog_set_disable(struct crashlog_entry *crashlog, bool disable) { - pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_DISABLE, disable); + pmt_crashlog_rmw(crashlog, crashlog->info->control.disable, disable); } static void pmt_crashlog_set_clear(struct crashlog_entry *crashlog) { - pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_TRIGGER_CLEAR, true); + pmt_crashlog_rmw(crashlog, crashlog->info->control.clear, true); } static void pmt_crashlog_set_execute(struct crashlog_entry *crashlog) { - pmt_crashlog_rmw(crashlog, CRASHLOG_FLAG_TRIGGER_EXECUTE, true); + pmt_crashlog_rmw(crashlog, crashlog->info->control.manual, true); } /* @@ -224,6 +250,19 @@ static const struct attribute_group pmt_crashlog_group = { .attrs = pmt_crashlog_attrs, }; +static const struct crashlog_info crashlog_type1_ver0 = { + .status.offset = TYPE1_VER0_STATUS_OFFSET, + .status.cleared = TYPE1_VER0_CLEAR, + .status.complete = TYPE1_VER0_COMPLETE, + .status.disabled = TYPE1_VER0_DISABLE, + + .control.offset = TYPE1_VER0_CONTROL_OFFSET, + .control.trigger_mask = TYPE1_VER0_TRIGGER_MASK, + .control.clear = TYPE1_VER0_CLEAR, + .control.disable = TYPE1_VER0_DISABLE, + .control.manual = TYPE1_VER0_EXECUTE, +}; + static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, struct device *dev) { @@ -234,9 +273,10 @@ static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, if (!pmt_crashlog_supported(entry)) return 1; - /* initialize control mutex */ + /* initialize the crashlog struct */ crashlog = container_of(entry, struct crashlog_entry, entry); mutex_init(&crashlog->control_mutex); + crashlog->info = &crashlog_type1_ver0; header->access_type = GET_ACCESS(readl(disc_table)); header->guid = readl(disc_table + GUID_OFFSET); -- cgit v1.2.3 From 2c402a801c19568de97b86a77b25d13448dc080a Mon Sep 17 00:00:00 2001 From: "Michael J. Ruhl" Date: Sun, 13 Jul 2025 13:29:43 -0400 Subject: platform/x86/intel/pmt: support BMG crashlog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Battlemage GPU has the type 1 version 2 crashlog feature. Update the crashlog driver to support this crashlog version. Signed-off-by: Michael J. Ruhl Link: https://lore.kernel.org/r/20250713172943.7335-14-michael.j.ruhl@intel.com [ij: make crashlog_type1_ver2 static] Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/intel/pmt/crashlog.c | 262 ++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 13 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/intel/pmt/crashlog.c b/drivers/platform/x86/intel/pmt/crashlog.c index db54f881ba57..b0393c9c5b4b 100644 --- a/drivers/platform/x86/intel/pmt/crashlog.c +++ b/drivers/platform/x86/intel/pmt/crashlog.c @@ -53,25 +53,59 @@ #define TYPE1_VER0_COMPLETE BIT(31) #define TYPE1_VER0_TRIGGER_MASK GENMASK(31, 28) +/* + * Type 1 Version 2 + * status and control are different registers + */ +#define TYPE1_VER2_STATUS_OFFSET 0x00 +#define TYPE1_VER2_CONTROL_OFFSET 0x14 + +/* status register */ +#define TYPE1_VER2_CLEAR_SUPPORT BIT(20) +#define TYPE1_VER2_REARMED BIT(25) +#define TYPE1_VER2_ERROR BIT(26) +#define TYPE1_VER2_CONSUMED BIT(27) +#define TYPE1_VER2_DISABLED BIT(28) +#define TYPE1_VER2_CLEARED BIT(29) +#define TYPE1_VER2_IN_PROGRESS BIT(30) +#define TYPE1_VER2_COMPLETE BIT(31) + +/* control register */ +#define TYPE1_VER2_CONSUME BIT(25) +#define TYPE1_VER2_REARM BIT(28) +#define TYPE1_VER2_EXECUTE BIT(29) +#define TYPE1_VER2_CLEAR BIT(30) +#define TYPE1_VER2_DISABLE BIT(31) +#define TYPE1_VER2_TRIGGER_MASK \ + (TYPE1_VER2_EXECUTE | TYPE1_VER2_CLEAR | TYPE1_VER2_DISABLE) + /* After offset, order alphabetically, not bit ordered */ struct crashlog_status { u32 offset; + u32 clear_supported; u32 cleared; u32 complete; + u32 consumed; u32 disabled; + u32 error; + u32 in_progress; + u32 rearmed; }; struct crashlog_control { u32 offset; u32 trigger_mask; u32 clear; + u32 consume; u32 disable; u32 manual; + u32 rearm; }; struct crashlog_info { const struct crashlog_status status; const struct crashlog_control control; + const struct attribute_group *attr_grp; }; struct crashlog_entry { @@ -128,19 +162,23 @@ static bool pmt_crashlog_disabled(struct crashlog_entry *crashlog) return pmt_crashlog_rc(crashlog, crashlog->info->status.disabled); } -static bool pmt_crashlog_supported(struct intel_pmt_entry *entry) +static bool pmt_crashlog_supported(struct intel_pmt_entry *entry, u32 *crash_type, u32 *version) { u32 discovery_header = readl(entry->disc_table + CONTROL_OFFSET); - u32 crash_type, version; - crash_type = GET_TYPE(discovery_header); - version = GET_VERSION(discovery_header); + *crash_type = GET_TYPE(discovery_header); + *version = GET_VERSION(discovery_header); /* - * Currently we only recognize OOBMSM version 0 devices. - * We can ignore all other crashlog devices in the system. + * Currently we only recognize OOBMSM (type 1) and version 0 or 2 + * devices. + * + * Ignore all other crashlog devices in the system. */ - return crash_type == CRASH_TYPE_OOBMSM && version == 0; + if (*crash_type == CRASH_TYPE_OOBMSM && (*version == 0 || *version == 2)) + return true; + + return false; } static void pmt_crashlog_set_disable(struct crashlog_entry *crashlog, @@ -159,9 +197,115 @@ static void pmt_crashlog_set_execute(struct crashlog_entry *crashlog) pmt_crashlog_rmw(crashlog, crashlog->info->control.manual, true); } +static bool pmt_crashlog_cleared(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.cleared); +} + +static bool pmt_crashlog_consumed(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.consumed); +} + +static void pmt_crashlog_set_consumed(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.consume, true); +} + +static bool pmt_crashlog_error(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.error); +} + +static bool pmt_crashlog_rearm(struct crashlog_entry *crashlog) +{ + return pmt_crashlog_rc(crashlog, crashlog->info->status.rearmed); +} + +static void pmt_crashlog_set_rearm(struct crashlog_entry *crashlog) +{ + pmt_crashlog_rmw(crashlog, crashlog->info->control.rearm, true); +} + /* * sysfs */ +static ssize_t +clear_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool cleared = pmt_crashlog_cleared(crashlog); + + return sysfs_emit(buf, "%d\n", cleared); +} + +static ssize_t +clear_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct crashlog_entry *crashlog; + bool clear; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &clear); + if (result) + return result; + + /* set bit only */ + if (!clear) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + pmt_crashlog_set_clear(crashlog); + + return count; +} +static DEVICE_ATTR_RW(clear); + +static ssize_t +consumed_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool consumed = pmt_crashlog_consumed(crashlog); + + return sysfs_emit(buf, "%d\n", consumed); +} + +static ssize_t +consumed_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct crashlog_entry *crashlog; + bool consumed; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &consumed); + if (result) + return result; + + /* set bit only */ + if (!consumed) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + if (pmt_crashlog_disabled(crashlog)) + return -EBUSY; + + if (!pmt_crashlog_complete(crashlog)) + return -EEXIST; + + pmt_crashlog_set_consumed(crashlog); + + return count; +} +static DEVICE_ATTR_RW(consumed); + static ssize_t enable_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -193,6 +337,51 @@ enable_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RW(enable); +static ssize_t +error_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + bool error = pmt_crashlog_error(crashlog); + + return sysfs_emit(buf, "%d\n", error); +} +static DEVICE_ATTR_RO(error); + +static ssize_t +rearm_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct crashlog_entry *crashlog = dev_get_drvdata(dev); + int rearmed = pmt_crashlog_rearm(crashlog); + + return sysfs_emit(buf, "%d\n", rearmed); +} + +static ssize_t +rearm_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct crashlog_entry *crashlog; + bool rearm; + int result; + + crashlog = dev_get_drvdata(dev); + + result = kstrtobool(buf, &rearm); + if (result) + return result; + + /* set only */ + if (!rearm) + return -EINVAL; + + guard(mutex)(&crashlog->control_mutex); + + pmt_crashlog_set_rearm(crashlog); + + return count; +} +static DEVICE_ATTR_RW(rearm); + static ssize_t trigger_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -240,14 +429,28 @@ trigger_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RW(trigger); -static struct attribute *pmt_crashlog_attrs[] = { +static struct attribute *pmt_crashlog_type1_ver0_attrs[] = { &dev_attr_enable.attr, &dev_attr_trigger.attr, NULL }; -static const struct attribute_group pmt_crashlog_group = { - .attrs = pmt_crashlog_attrs, +static struct attribute *pmt_crashlog_type1_ver2_attrs[] = { + &dev_attr_clear.attr, + &dev_attr_consumed.attr, + &dev_attr_enable.attr, + &dev_attr_error.attr, + &dev_attr_rearm.attr, + &dev_attr_trigger.attr, + NULL +}; + +static const struct attribute_group pmt_crashlog_type1_ver0_group = { + .attrs = pmt_crashlog_type1_ver0_attrs, +}; + +static const struct attribute_group pmt_crashlog_type1_ver2_group = { + .attrs = pmt_crashlog_type1_ver2_attrs, }; static const struct crashlog_info crashlog_type1_ver0 = { @@ -261,22 +464,55 @@ static const struct crashlog_info crashlog_type1_ver0 = { .control.clear = TYPE1_VER0_CLEAR, .control.disable = TYPE1_VER0_DISABLE, .control.manual = TYPE1_VER0_EXECUTE, + .attr_grp = &pmt_crashlog_type1_ver0_group, }; +static const struct crashlog_info crashlog_type1_ver2 = { + .status.offset = TYPE1_VER2_STATUS_OFFSET, + .status.clear_supported = TYPE1_VER2_CLEAR_SUPPORT, + .status.cleared = TYPE1_VER2_CLEARED, + .status.complete = TYPE1_VER2_COMPLETE, + .status.consumed = TYPE1_VER2_CONSUMED, + .status.disabled = TYPE1_VER2_DISABLED, + .status.error = TYPE1_VER2_ERROR, + .status.in_progress = TYPE1_VER2_IN_PROGRESS, + .status.rearmed = TYPE1_VER2_REARMED, + + .control.offset = TYPE1_VER2_CONTROL_OFFSET, + .control.trigger_mask = TYPE1_VER2_TRIGGER_MASK, + .control.clear = TYPE1_VER2_CLEAR, + .control.consume = TYPE1_VER2_CONSUME, + .control.disable = TYPE1_VER2_DISABLE, + .control.manual = TYPE1_VER2_EXECUTE, + .control.rearm = TYPE1_VER2_REARM, + .attr_grp = &pmt_crashlog_type1_ver2_group, +}; + +static const struct crashlog_info *select_crashlog_info(u32 type, u32 version) +{ + if (version == 0) + return &crashlog_type1_ver0; + + return &crashlog_type1_ver2; +} + static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, struct device *dev) { void __iomem *disc_table = entry->disc_table; struct intel_pmt_header *header = &entry->header; struct crashlog_entry *crashlog; + u32 version; + u32 type; - if (!pmt_crashlog_supported(entry)) + if (!pmt_crashlog_supported(entry, &type, &version)) return 1; /* initialize the crashlog struct */ crashlog = container_of(entry, struct crashlog_entry, entry); mutex_init(&crashlog->control_mutex); - crashlog->info = &crashlog_type1_ver0; + + crashlog->info = select_crashlog_info(type, version); header->access_type = GET_ACCESS(readl(disc_table)); header->guid = readl(disc_table + GUID_OFFSET); @@ -285,7 +521,7 @@ static int pmt_crashlog_header_decode(struct intel_pmt_entry *entry, /* Size is measured in DWORDS, but accessor returns bytes */ header->size = GET_SIZE(readl(disc_table + SIZE_OFFSET)); - entry->attr_grp = &pmt_crashlog_group; + entry->attr_grp = crashlog->info->attr_grp; return 0; } -- cgit v1.2.3 From 232b41d3c2ce8cf4641a174416676458bf0de5b2 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Fri, 18 Jul 2025 18:33:04 +0200 Subject: platform/x86: oxpec: Fix turbo register for G1 AMD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out that the AMD variant of the G1 uses different EC registers than the Intel variant. Differentiate them and apply the correct ones to the AMD variant. Fixes: b369395c895b ("platform/x86: oxpec: Add support for the OneXPlayer G1") Signed-off-by: Antheas Kapenekakis Link: https://lore.kernel.org/r/20250718163305.159232-1-lkml@antheas.dev Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/oxpec.c | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) (limited to 'drivers') diff --git a/drivers/platform/x86/oxpec.c b/drivers/platform/x86/oxpec.c index 06759036945d..9839e8cb82ce 100644 --- a/drivers/platform/x86/oxpec.c +++ b/drivers/platform/x86/oxpec.c @@ -58,7 +58,8 @@ enum oxp_board { oxp_mini_amd_a07, oxp_mini_amd_pro, oxp_x1, - oxp_g1, + oxp_g1_i, + oxp_g1_a, }; static enum oxp_board board; @@ -247,14 +248,14 @@ static const struct dmi_system_id dmi_table[] = { DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), }, - .driver_data = (void *)oxp_g1, + .driver_data = (void *)oxp_g1_a, }, { .matches = { DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 i"), }, - .driver_data = (void *)oxp_g1, + .driver_data = (void *)oxp_g1_i, }, { .matches = { @@ -352,7 +353,8 @@ static umode_t tt_toggle_is_visible(struct kobject *kobj, case oxp_mini_amd_a07: case oxp_mini_amd_pro: case oxp_x1: - case oxp_g1: + case oxp_g1_i: + case oxp_g1_a: return attr->mode; default: break; @@ -381,12 +383,13 @@ static ssize_t tt_toggle_store(struct device *dev, case aok_zoe_a1: case oxp_fly: case oxp_mini_amd_pro: + case oxp_g1_a: reg = OXP_TURBO_SWITCH_REG; mask = OXP_TURBO_TAKE_VAL; break; case oxp_2: case oxp_x1: - case oxp_g1: + case oxp_g1_i: reg = OXP_2_TURBO_SWITCH_REG; mask = OXP_TURBO_TAKE_VAL; break; @@ -426,12 +429,13 @@ static ssize_t tt_toggle_show(struct device *dev, case aok_zoe_a1: case oxp_fly: case oxp_mini_amd_pro: + case oxp_g1_a: reg = OXP_TURBO_SWITCH_REG; mask = OXP_TURBO_TAKE_VAL; break; case oxp_2: case oxp_x1: - case oxp_g1: + case oxp_g1_i: reg = OXP_2_TURBO_SWITCH_REG; mask = OXP_TURBO_TAKE_VAL; break; @@ -520,7 +524,8 @@ static bool oxp_psy_ext_supported(void) { switch (board) { case oxp_x1: - case oxp_g1: + case oxp_g1_i: + case oxp_g1_a: case oxp_fly: return true; default: @@ -659,7 +664,8 @@ static int oxp_pwm_enable(void) case oxp_mini_amd_a07: case oxp_mini_amd_pro: case oxp_x1: - case oxp_g1: + case oxp_g1_i: + case oxp_g1_a: return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, PWM_MODE_MANUAL); default: return -EINVAL; @@ -686,7 +692,8 @@ static int oxp_pwm_disable(void) case oxp_mini_amd_a07: case oxp_mini_amd_pro: case oxp_x1: - case oxp_g1: + case oxp_g1_i: + case oxp_g1_a: return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, PWM_MODE_AUTO); default: return -EINVAL; @@ -713,7 +720,8 @@ static int oxp_pwm_read(long *val) case oxp_mini_amd_a07: case oxp_mini_amd_pro: case oxp_x1: - case oxp_g1: + case oxp_g1_i: + case oxp_g1_a: return read_from_ec(OXP_SENSOR_PWM_ENABLE_REG, 1, val); default: return -EOPNOTSUPP; @@ -742,7 +750,7 @@ static int oxp_pwm_fan_speed(long *val) return read_from_ec(ORANGEPI_SENSOR_FAN_REG, 2, val); case oxp_2: case oxp_x1: - case oxp_g1: + case oxp_g1_i: return read_from_ec(OXP_2_SENSOR_FAN_REG, 2, val); case aok_zoe_a1: case aya_neo_2: @@ -757,6 +765,7 @@ static int oxp_pwm_fan_speed(long *val) case oxp_mini_amd: case oxp_mini_amd_a07: case oxp_mini_amd_pro: + case oxp_g1_a: return read_from_ec(OXP_SENSOR_FAN_REG, 2, val); default: return -EOPNOTSUPP; @@ -776,7 +785,7 @@ static int oxp_pwm_input_write(long val) return write_to_ec(ORANGEPI_SENSOR_PWM_REG, val); case oxp_2: case oxp_x1: - case oxp_g1: + case oxp_g1_i: /* scale to range [0-184] */ val = (val * 184) / 255; return write_to_ec(OXP_SENSOR_PWM_REG, val); @@ -796,6 +805,7 @@ static int oxp_pwm_input_write(long val) case aok_zoe_a1: case oxp_fly: case oxp_mini_amd_pro: + case oxp_g1_a: return write_to_ec(OXP_SENSOR_PWM_REG, val); default: return -EOPNOTSUPP; @@ -816,7 +826,7 @@ static int oxp_pwm_input_read(long *val) break; case oxp_2: case oxp_x1: - case oxp_g1: + case oxp_g1_i: ret = read_from_ec(OXP_SENSOR_PWM_REG, 1, val); if (ret) return ret; @@ -842,6 +852,7 @@ static int oxp_pwm_input_read(long *val) case aok_zoe_a1: case oxp_fly: case oxp_mini_amd_pro: + case oxp_g1_a: default: ret = read_from_ec(OXP_SENSOR_PWM_REG, 1, val); if (ret) -- cgit v1.2.3 From 1798561befd8be1e52feb54f850efcab5a595f43 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Fri, 18 Jul 2025 18:33:05 +0200 Subject: platform/x86: oxpec: Add support for OneXPlayer X1 Mini Pro (Strix Point) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OneXPlayer X1 Mini Pro (which is the Strix Point variant of the Mini) uses the same registers as the X1 Mini, so re-use the quirk. Signed-off-by: Antheas Kapenekakis Link: https://lore.kernel.org/r/20250718163305.159232-2-lkml@antheas.dev Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- drivers/platform/x86/oxpec.c | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'drivers') diff --git a/drivers/platform/x86/oxpec.c b/drivers/platform/x86/oxpec.c index 9839e8cb82ce..eb076bb4099b 100644 --- a/drivers/platform/x86/oxpec.c +++ b/drivers/platform/x86/oxpec.c @@ -292,6 +292,13 @@ static const struct dmi_system_id dmi_table[] = { }, .driver_data = (void *)oxp_x1, }, + { + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Mini Pro"), + }, + .driver_data = (void *)oxp_x1, + }, { .matches = { DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), -- cgit v1.2.3