// SPDX-License-Identifier: GPL-2.0-or-later /* * Linux driver for WMI platform features on MSI notebooks. * * Copyright (C) 2024 Armin Wolf */ #define pr_format(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DRIVER_NAME "msi-wmi-platform" #define MSI_PLATFORM_GUID "ABBC0F6E-8EA1-11d1-00A0-C90629100000" #define MSI_WMI_PLATFORM_INTERFACE_VERSION 2 #define MSI_PLATFORM_WMI_MAJOR_OFFSET 1 #define MSI_PLATFORM_WMI_MINOR_OFFSET 2 #define MSI_PLATFORM_EC_FLAGS_OFFSET 1 #define MSI_PLATFORM_EC_MINOR_MASK GENMASK(3, 0) #define MSI_PLATFORM_EC_MAJOR_MASK GENMASK(5, 4) #define MSI_PLATFORM_EC_CHANGED_PAGE BIT(6) #define MSI_PLATFORM_EC_IS_TIGERLAKE BIT(7) #define MSI_PLATFORM_EC_VERSION_OFFSET 2 static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); enum msi_wmi_platform_method { MSI_PLATFORM_GET_PACKAGE = 0x01, MSI_PLATFORM_SET_PACKAGE = 0x02, MSI_PLATFORM_GET_EC = 0x03, MSI_PLATFORM_SET_EC = 0x04, MSI_PLATFORM_GET_BIOS = 0x05, MSI_PLATFORM_SET_BIOS = 0x06, MSI_PLATFORM_GET_SMBUS = 0x07, MSI_PLATFORM_SET_SMBUS = 0x08, MSI_PLATFORM_GET_MASTER_BATTERY = 0x09, MSI_PLATFORM_SET_MASTER_BATTERY = 0x0a, MSI_PLATFORM_GET_SLAVE_BATTERY = 0x0b, MSI_PLATFORM_SET_SLAVE_BATTERY = 0x0c, MSI_PLATFORM_GET_TEMPERATURE = 0x0d, MSI_PLATFORM_SET_TEMPERATURE = 0x0e, MSI_PLATFORM_GET_THERMAL = 0x0f, MSI_PLATFORM_SET_THERMAL = 0x10, MSI_PLATFORM_GET_FAN = 0x11, MSI_PLATFORM_SET_FAN = 0x12, MSI_PLATFORM_GET_DEVICE = 0x13, MSI_PLATFORM_SET_DEVICE = 0x14, MSI_PLATFORM_GET_POWER = 0x15, MSI_PLATFORM_SET_POWER = 0x16, MSI_PLATFORM_GET_DEBUG = 0x17, MSI_PLATFORM_SET_DEBUG = 0x18, MSI_PLATFORM_GET_AP = 0x19, MSI_PLATFORM_SET_AP = 0x1a, MSI_PLATFORM_GET_DATA = 0x1b, MSI_PLATFORM_SET_DATA = 0x1c, MSI_PLATFORM_GET_WMI = 0x1d, }; struct msi_wmi_platform_debugfs_data { struct wmi_device *wdev; enum msi_wmi_platform_method method; struct rw_semaphore buffer_lock; /* Protects debugfs buffer */ size_t length; u8 buffer[32]; }; static const char * const msi_wmi_platform_debugfs_names[] = { "get_package", "set_package", "get_ec", "set_ec", "get_bios", "set_bios", "get_smbus", "set_smbus", "get_master_battery", "set_master_battery", "get_slave_battery", "set_slave_battery", "get_temperature", "set_temperature", "get_thermal", "set_thermal", "get_fan", "set_fan", "get_device", "set_device", "get_power", "set_power", "get_debug", "set_debug", "get_ap", "set_ap", "get_data", "set_data", "get_wmi" }; static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, size_t length) { if (obj->type != ACPI_TYPE_BUFFER) return -ENOMSG; if (obj->buffer.length != length) return -EPROTO; if (!obj->buffer.pointer[0]) return -EIO; memcpy(output, obj->buffer.pointer, obj->buffer.length); return 0; } static int msi_wmi_platform_query(struct wmi_device *wdev, enum msi_wmi_platform_method method, u8 *input, size_t input_length, u8 *output, size_t output_length) { struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; struct acpi_buffer in = { .length = input_length, .pointer = input }; union acpi_object *obj; acpi_status status; int ret; if (!input_length || !output_length) return -EINVAL; status = wmidev_evaluate_method(wdev, 0x0, method, &in, &out); if (ACPI_FAILURE(status)) return -EIO; obj = out.pointer; if (!obj) return -ENODATA; ret = msi_wmi_platform_parse_buffer(obj, output, output_length); kfree(obj); return ret; } static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { return 0444; } static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { struct wmi_device *wdev = dev_get_drvdata(dev); u8 input[32] = { 0 }; u8 output[32]; u16 data; int ret; ret = msi_wmi_platform_query(wdev, MSI_PLATFORM_GET_FAN, input, sizeof(input), output, sizeof(output)); if (ret < 0) return ret; data = get_unaligned_be16(&output[channel * 2 + 1]); if (!data) *val = 0; else *val = 480000 / data; return 0; } static const struct hwmon_ops msi_wmi_platform_ops = { .is_visible = msi_wmi_platform_is_visible, .read = msi_wmi_platform_read, }; static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT, HWMON_F_INPUT, HWMON_F_INPUT, HWMON_F_INPUT ), NULL }; static const struct hwmon_chip_info msi_wmi_platform_chip_info = { .ops = &msi_wmi_platform_ops, .info = msi_wmi_platform_info, }; static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, size_t length, loff_t *offset) { struct seq_file *seq = fp->private_data; struct msi_wmi_platform_debugfs_data *data = seq->private; u8 payload[32] = { }; ssize_t ret; /* Do not allow partial writes */ if (*offset != 0) return -EINVAL; /* Do not allow incomplete command buffers */ if (length != data->length) return -EINVAL; ret = simple_write_to_buffer(payload, sizeof(payload), offset, input, length); if (ret < 0) return ret; down_write(&data->buffer_lock); ret = msi_wmi_platform_query(data->wdev, data->method, payload, data->length, data->buffer, data->length); up_write(&data->buffer_lock); if (ret < 0) return ret; return length; } static int msi_wmi_platform_show(struct seq_file *seq, void *p) { struct msi_wmi_platform_debugfs_data *data = seq->private; int ret; down_read(&data->buffer_lock); ret = seq_write(seq, data->buffer, data->length); up_read(&data->buffer_lock); return ret; } static int msi_wmi_platform_open(struct inode *inode, struct file *fp) { struct msi_wmi_platform_debugfs_data *data = inode->i_private; /* The seq_file uses the last byte of the buffer for detecting buffer overflows */ return single_open_size(fp, msi_wmi_platform_show, data, data->length + 1); } static const struct file_operations msi_wmi_platform_debugfs_fops = { .owner = THIS_MODULE, .open = msi_wmi_platform_open, .read = seq_read, .write = msi_wmi_platform_write, .llseek = seq_lseek, .release = single_release, }; static void msi_wmi_platform_debugfs_remove(void *data) { struct dentry *dir = data; debugfs_remove_recursive(dir); } static void msi_wmi_platform_debugfs_add(struct wmi_device *wdev, struct dentry *dir, const char *name, enum msi_wmi_platform_method method) { struct msi_wmi_platform_debugfs_data *data; struct dentry *entry; data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); if (!data) return; data->wdev = wdev; data->method = method; init_rwsem(&data->buffer_lock); /* The ACPI firmware for now always requires a 32 byte input buffer due to * a peculiarity in how Windows handles the CreateByteField() ACPI operator. */ data->length = 32; entry = debugfs_create_file(name, 0600, dir, data, &msi_wmi_platform_debugfs_fops); if (IS_ERR(entry)) devm_kfree(&wdev->dev, data); } static void msi_wmi_platform_debugfs_init(struct wmi_device *wdev) { struct dentry *dir; char dir_name[64]; int ret, method; scnprintf(dir_name, ARRAY_SIZE(dir_name), "%s-%s", DRIVER_NAME, dev_name(&wdev->dev)); dir = debugfs_create_dir(dir_name, NULL); if (IS_ERR(dir)) return; ret = devm_add_action_or_reset(&wdev->dev, msi_wmi_platform_debugfs_remove, dir); if (ret < 0) return; for (method = MSI_PLATFORM_GET_PACKAGE; method <= MSI_PLATFORM_GET_WMI; method++) msi_wmi_platform_debugfs_add(wdev, dir, msi_wmi_platform_debugfs_names[method - 1], method); } static int msi_wmi_platform_hwmon_init(struct wmi_device *wdev) { struct device *hdev; hdev = devm_hwmon_device_register_with_info(&wdev->dev, "msi_wmi_platform", wdev, &msi_wmi_platform_chip_info, NULL); return PTR_ERR_OR_ZERO(hdev); } static int msi_wmi_platform_ec_init(struct wmi_device *wdev) { u8 input[32] = { 0 }; u8 output[32]; u8 flags; int ret; ret = msi_wmi_platform_query(wdev, MSI_PLATFORM_GET_EC, input, sizeof(input), output, sizeof(output)); if (ret < 0) return ret; flags = output[MSI_PLATFORM_EC_FLAGS_OFFSET]; dev_dbg(&wdev->dev, "EC RAM version %lu.%lu\n", FIELD_GET(MSI_PLATFORM_EC_MAJOR_MASK, flags), FIELD_GET(MSI_PLATFORM_EC_MINOR_MASK, flags)); dev_dbg(&wdev->dev, "EC firmware version %.28s\n", &output[MSI_PLATFORM_EC_VERSION_OFFSET]); if (!(flags & MSI_PLATFORM_EC_IS_TIGERLAKE)) { if (!force) return -ENODEV; dev_warn(&wdev->dev, "Loading on a non-Tigerlake platform\n"); } return 0; } static int msi_wmi_platform_init(struct wmi_device *wdev) { u8 input[32] = { 0 }; u8 output[32]; int ret; ret = msi_wmi_platform_query(wdev, MSI_PLATFORM_GET_WMI, input, sizeof(input), output, sizeof(output)); if (ret < 0) return ret; dev_dbg(&wdev->dev, "WMI interface version %u.%u\n", output[MSI_PLATFORM_WMI_MAJOR_OFFSET], output[MSI_PLATFORM_WMI_MINOR_OFFSET]); if (output[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) { if (!force) return -ENODEV; dev_warn(&wdev->dev, "Loading despite unsupported WMI interface version (%u.%u)\n", output[MSI_PLATFORM_WMI_MAJOR_OFFSET], output[MSI_PLATFORM_WMI_MINOR_OFFSET]); } return 0; } static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) { int ret; ret = msi_wmi_platform_init(wdev); if (ret < 0) return ret; ret = msi_wmi_platform_ec_init(wdev); if (ret < 0) return ret; msi_wmi_platform_debugfs_init(wdev); return msi_wmi_platform_hwmon_init(wdev); } static const struct wmi_device_id msi_wmi_platform_id_table[] = { { MSI_PLATFORM_GUID, NULL }, { } }; MODULE_DEVICE_TABLE(wmi, msi_wmi_platform_id_table); static struct wmi_driver msi_wmi_platform_driver = { .driver = { .name = DRIVER_NAME, .probe_type = PROBE_PREFER_ASYNCHRONOUS, }, .id_table = msi_wmi_platform_id_table, .probe = msi_wmi_platform_probe, .no_singleton = true, }; module_wmi_driver(msi_wmi_platform_driver); MODULE_AUTHOR("Armin Wolf "); MODULE_DESCRIPTION("MSI WMI platform features"); MODULE_LICENSE("GPL");