hwmon: (dell-smm) Add support for WMI SMM interface
authorArmin Wolf <W_Armin@gmx.de>
Thu, 23 Nov 2023 00:48:18 +0000 (01:48 +0100)
committerGuenter Roeck <linux@roeck-us.net>
Mon, 11 Dec 2023 14:21:01 +0000 (06:21 -0800)
Some Dell machines like the Dell Optiplex 7000 do not support
the legacy SMM interface, but instead expect all SMM calls
to be issued over a special WMI interface.
Add support for this interface so users can control the fans
on those machines.

Tested-by: <serverror@serverror.com>
Reviewed-by: Hans de Goede <hdegoede@redhat.com>
Reviewed-by: Pali Rohár <pali@kernel.org>
Signed-off-by: Armin Wolf <W_Armin@gmx.de>
Link: https://lore.kernel.org/r/20231123004820.50635-8-W_Armin@gmx.de
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
drivers/hwmon/Kconfig
drivers/hwmon/dell-smm-hwmon.c
drivers/platform/x86/wmi.c

index cf27523eed5a7133d60d4690d3e9065c63e651b7..76cb05db1dcf3c8c4f6d3a542f14f6c050dc6929 100644 (file)
@@ -512,6 +512,7 @@ config SENSORS_DS1621
 
 config SENSORS_DELL_SMM
        tristate "Dell laptop SMM BIOS hwmon driver"
+       depends on ACPI_WMI
        depends on X86
        imply THERMAL
        help
index a377cd08355f0ce31f5e5d1851f72d34f0a8ab14..95330437c5af728e13a7bdd1b598a5e2adcf77da 100644 (file)
@@ -12,6 +12,7 @@
 
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
+#include <linux/acpi.h>
 #include <linux/capability.h>
 #include <linux/cpu.h>
 #include <linux/ctype.h>
 #include <linux/thermal.h>
 #include <linux/types.h>
 #include <linux/uaccess.h>
+#include <linux/wmi.h>
 
 #include <linux/i8k.h>
+#include <asm/unaligned.h>
 
 #define I8K_SMM_FN_STATUS      0x0025
 #define I8K_SMM_POWER_STATUS   0x0069
@@ -66,6 +69,9 @@
 #define I8K_POWER_AC           0x05
 #define I8K_POWER_BATTERY      0x01
 
+#define DELL_SMM_WMI_GUID      "F1DDEE52-063C-4784-A11E-8A06684B9B01"
+#define DELL_SMM_LEGACY_EXECUTE        0x1
+
 #define DELL_SMM_NO_TEMP       10
 #define DELL_SMM_NO_FANS       3
 
@@ -219,6 +225,103 @@ static const struct dell_smm_ops i8k_smm_ops = {
        .smm_call = i8k_smm_call,
 };
 
+/*
+ * Call the System Management Mode BIOS over WMI.
+ */
+static ssize_t wmi_parse_register(u8 *buffer, u32 length, unsigned int *reg)
+{
+       __le32 value;
+       u32 reg_size;
+
+       if (length <= sizeof(reg_size))
+               return -ENODATA;
+
+       reg_size = get_unaligned_le32(buffer);
+       if (!reg_size || reg_size > sizeof(value))
+               return -ENOMSG;
+
+       if (length < sizeof(reg_size) + reg_size)
+               return -ENODATA;
+
+       memcpy_and_pad(&value, sizeof(value), buffer + sizeof(reg_size), reg_size, 0);
+       *reg = le32_to_cpu(value);
+
+       return reg_size + sizeof(reg_size);
+}
+
+static int wmi_parse_response(u8 *buffer, u32 length, struct smm_regs *regs)
+{
+       unsigned int *registers[] = {
+               &regs->eax,
+               &regs->ebx,
+               &regs->ecx,
+               &regs->edx
+       };
+       u32 offset = 0;
+       ssize_t ret;
+       int i;
+
+       for (i = 0; i < ARRAY_SIZE(registers); i++) {
+               if (offset >= length)
+                       return -ENODATA;
+
+               ret = wmi_parse_register(buffer + offset, length - offset, registers[i]);
+               if (ret < 0)
+                       return ret;
+
+               offset += ret;
+       }
+
+       if (offset != length)
+               return -ENOMSG;
+
+       return 0;
+}
+
+static int wmi_smm_call(struct device *dev, struct smm_regs *regs)
+{
+       struct wmi_device *wdev = container_of(dev, struct wmi_device, dev);
+       struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
+       u32 wmi_payload[] = {
+               sizeof(regs->eax),
+               regs->eax,
+               sizeof(regs->ebx),
+               regs->ebx,
+               sizeof(regs->ecx),
+               regs->ecx,
+               sizeof(regs->edx),
+               regs->edx
+       };
+       const struct acpi_buffer in = {
+               .length = sizeof(wmi_payload),
+               .pointer = &wmi_payload,
+       };
+       union acpi_object *obj;
+       acpi_status status;
+       int ret;
+
+       status = wmidev_evaluate_method(wdev, 0x0, DELL_SMM_LEGACY_EXECUTE, &in, &out);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+
+       obj = out.pointer;
+       if (!obj)
+               return -ENODATA;
+
+       if (obj->type != ACPI_TYPE_BUFFER) {
+               ret = -ENOMSG;
+
+               goto err_free;
+       }
+
+       ret = wmi_parse_response(obj->buffer.pointer, obj->buffer.length, regs);
+
+err_free:
+       kfree(obj);
+
+       return ret;
+}
+
 static int dell_smm_call(const struct dell_smm_ops *ops, struct smm_regs *regs)
 {
        unsigned int eax = regs->eax;
@@ -306,7 +409,7 @@ static int i8k_get_fan_type(struct dell_smm_data *data, u8 fan)
 /*
  * Read the fan nominal rpm for specific fan speed.
  */
-static int __init i8k_get_fan_nominal_speed(const struct dell_smm_data *data, u8 fan, int speed)
+static int i8k_get_fan_nominal_speed(const struct dell_smm_data *data, u8 fan, int speed)
 {
        struct smm_regs regs = {
                .eax = I8K_SMM_GET_NOM_SPEED,
@@ -349,7 +452,7 @@ static int i8k_set_fan(const struct dell_smm_data *data, u8 fan, int speed)
        return dell_smm_call(data->ops, &regs);
 }
 
-static int __init i8k_get_temp_type(const struct dell_smm_data *data, u8 sensor)
+static int i8k_get_temp_type(const struct dell_smm_data *data, u8 sensor)
 {
        struct smm_regs regs = {
                .eax = I8K_SMM_GET_TEMP_TYPE,
@@ -401,7 +504,7 @@ static int i8k_get_temp(const struct dell_smm_data *data, u8 sensor)
        return temp;
 }
 
-static int __init dell_smm_get_signature(const struct dell_smm_ops *ops, int req_fn)
+static int dell_smm_get_signature(const struct dell_smm_ops *ops, int req_fn)
 {
        struct smm_regs regs = { .eax = req_fn, };
        int rc;
@@ -986,7 +1089,7 @@ static const struct hwmon_chip_info dell_smm_chip_info = {
        .info = dell_smm_info,
 };
 
-static int __init dell_smm_init_cdev(struct device *dev, u8 fan_num)
+static int dell_smm_init_cdev(struct device *dev, u8 fan_num)
 {
        struct dell_smm_data *data = dev_get_drvdata(dev);
        struct thermal_cooling_device *cdev;
@@ -1017,7 +1120,7 @@ static int __init dell_smm_init_cdev(struct device *dev, u8 fan_num)
        return ret;
 }
 
-static int __init dell_smm_init_hwmon(struct device *dev)
+static int dell_smm_init_hwmon(struct device *dev)
 {
        struct dell_smm_data *data = dev_get_drvdata(dev);
        struct device *dell_smm_hwmon_dev;
@@ -1083,7 +1186,7 @@ static int __init dell_smm_init_hwmon(struct device *dev)
        return PTR_ERR_OR_ZERO(dell_smm_hwmon_dev);
 }
 
-static int __init dell_smm_init_data(struct device *dev, const struct dell_smm_ops *ops)
+static int dell_smm_init_data(struct device *dev, const struct dell_smm_ops *ops)
 {
        struct dell_smm_data *data;
 
@@ -1409,6 +1512,9 @@ static const struct dmi_system_id i8k_whitelist_fan_control[] __initconst = {
        { }
 };
 
+/*
+ * Legacy SMM backend driver.
+ */
 static int __init dell_smm_probe(struct platform_device *pdev)
 {
        int ret;
@@ -1434,6 +1540,47 @@ static struct platform_driver dell_smm_driver = {
 
 static struct platform_device *dell_smm_device;
 
+/*
+ * WMI SMM backend driver.
+ */
+static int dell_smm_wmi_probe(struct wmi_device *wdev, const void *context)
+{
+       struct dell_smm_ops *ops;
+       int ret;
+
+       ops = devm_kzalloc(&wdev->dev, sizeof(*ops), GFP_KERNEL);
+       if (!ops)
+               return -ENOMEM;
+
+       ops->smm_call = wmi_smm_call;
+       ops->smm_dev = &wdev->dev;
+
+       if (dell_smm_get_signature(ops, I8K_SMM_GET_DELL_SIG1) &&
+           dell_smm_get_signature(ops, I8K_SMM_GET_DELL_SIG2))
+               return -ENODEV;
+
+       ret = dell_smm_init_data(&wdev->dev, ops);
+       if (ret < 0)
+               return ret;
+
+       return dell_smm_init_hwmon(&wdev->dev);
+}
+
+static const struct wmi_device_id dell_smm_wmi_id_table[] = {
+       { DELL_SMM_WMI_GUID, NULL },
+       { }
+};
+MODULE_DEVICE_TABLE(wmi, dell_smm_wmi_id_table);
+
+static struct wmi_driver dell_smm_wmi_driver = {
+       .driver = {
+               .name = KBUILD_MODNAME,
+               .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+       },
+       .id_table = dell_smm_wmi_id_table,
+       .probe = dell_smm_wmi_probe,
+};
+
 /*
  * Probe for the presence of a supported laptop.
  */
@@ -1485,33 +1632,43 @@ static void __init dell_smm_init_dmi(void)
        }
 }
 
-static int __init i8k_init(void)
+static int __init dell_smm_legacy_check(void)
 {
-       /*
-        * Get DMI information
-        */
        if (!dmi_check_system(i8k_dmi_table)) {
                if (!ignore_dmi && !force)
                        return -ENODEV;
 
-               pr_info("not running on a supported Dell system.\n");
+               pr_info("Probing for legacy SMM handler on unsupported machine\n");
                pr_info("vendor=%s, model=%s, version=%s\n",
                        i8k_get_dmi_data(DMI_SYS_VENDOR),
                        i8k_get_dmi_data(DMI_PRODUCT_NAME),
                        i8k_get_dmi_data(DMI_BIOS_VERSION));
        }
 
-       dell_smm_init_dmi();
-
-       /*
-        * Get SMM Dell signature
-        */
        if (dell_smm_get_signature(&i8k_smm_ops, I8K_SMM_GET_DELL_SIG1) &&
            dell_smm_get_signature(&i8k_smm_ops, I8K_SMM_GET_DELL_SIG2)) {
                if (!force)
                        return -ENODEV;
 
-               pr_err("Unable to get Dell SMM signature\n");
+               pr_warn("Forcing legacy SMM calls on a possibly incompatible machine\n");
+       }
+
+       return 0;
+}
+
+static int __init i8k_init(void)
+{
+       int ret;
+
+       dell_smm_init_dmi();
+
+       ret = dell_smm_legacy_check();
+       if (ret < 0) {
+               /*
+                * On modern machines, SMM communication happens over WMI, meaning
+                * the SMM handler might not react to legacy SMM calls.
+                */
+               return wmi_driver_register(&dell_smm_wmi_driver);
        }
 
        dell_smm_device = platform_create_bundle(&dell_smm_driver, dell_smm_probe, NULL, 0, NULL,
@@ -1522,8 +1679,12 @@ static int __init i8k_init(void)
 
 static void __exit i8k_exit(void)
 {
-       platform_device_unregister(dell_smm_device);
-       platform_driver_unregister(&dell_smm_driver);
+       if (dell_smm_device) {
+               platform_device_unregister(dell_smm_device);
+               platform_driver_unregister(&dell_smm_driver);
+       } else {
+               wmi_driver_unregister(&dell_smm_wmi_driver);
+       }
 }
 
 module_init(i8k_init);
index 5dd22258cb3bc2b350331ce07431b8ecf6c45734..b054f68ead86cf29ed033641fbaa2e937df7bc05 100644 (file)
@@ -106,6 +106,7 @@ MODULE_DEVICE_TABLE(acpi, wmi_device_ids);
 static const char * const allow_duplicates[] = {
        "05901221-D566-11D1-B2F0-00A0C9062910", /* wmi-bmof */
        "8A42EA14-4F2A-FD45-6422-0087F7A7E608", /* dell-wmi-ddv */
+       "F1DDEE52-063C-4784-A11E-8A06684B9B01", /* dell-smm-hwmon */
        NULL
 };