| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- // SPDX-License-Identifier: GPL-2.0-or-later
- /*
- * Linux driver for WMI platform features on MSI notebooks.
- *
- * Copyright (C) 2024 Armin Wolf <W_Armin@gmx.de>
- */
- #define pr_format(fmt) KBUILD_MODNAME ": " fmt
- #include <linux/acpi.h>
- #include <linux/bits.h>
- #include <linux/bitfield.h>
- #include <linux/cleanup.h>
- #include <linux/debugfs.h>
- #include <linux/device.h>
- #include <linux/device/driver.h>
- #include <linux/dmi.h>
- #include <linux/errno.h>
- #include <linux/hwmon.h>
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/mutex.h>
- #include <linux/printk.h>
- #include <linux/rwsem.h>
- #include <linux/types.h>
- #include <linux/wmi.h>
- #include <linux/unaligned.h>
- #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_data {
- struct wmi_device *wdev;
- struct mutex wmi_lock; /* Necessary when calling WMI methods */
- };
- struct msi_wmi_platform_debugfs_data {
- struct msi_wmi_platform_data *data;
- 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 msi_wmi_platform_data *data,
- 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;
- /*
- * The ACPI control method responsible for handling the WMI method calls
- * is not thread-safe. Because of this we have to do the locking ourself.
- */
- scoped_guard(mutex, &data->wmi_lock) {
- status = wmidev_evaluate_method(data->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 msi_wmi_platform_data *data = dev_get_drvdata(dev);
- u8 input[32] = { 0 };
- u8 output[32];
- u16 value;
- int ret;
- ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, input, sizeof(input), output,
- sizeof(output));
- if (ret < 0)
- return ret;
- value = get_unaligned_be16(&output[channel * 2 + 1]);
- if (!value)
- *val = 0;
- else
- *val = 480000 / value;
- 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->data, 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 msi_wmi_platform_data *drvdata, 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(&drvdata->wdev->dev, sizeof(*data), GFP_KERNEL);
- if (!data)
- return;
- data->data = drvdata;
- 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(&drvdata->wdev->dev, data);
- }
- static void msi_wmi_platform_debugfs_init(struct msi_wmi_platform_data *data)
- {
- struct dentry *dir;
- char dir_name[64];
- int ret, method;
- scnprintf(dir_name, ARRAY_SIZE(dir_name), "%s-%s", DRIVER_NAME, dev_name(&data->wdev->dev));
- dir = debugfs_create_dir(dir_name, NULL);
- if (IS_ERR(dir))
- return;
- ret = devm_add_action_or_reset(&data->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(data, dir, msi_wmi_platform_debugfs_names[method - 1],
- method);
- }
- static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data)
- {
- struct device *hdev;
- hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data,
- &msi_wmi_platform_chip_info, NULL);
- return PTR_ERR_OR_ZERO(hdev);
- }
- static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data)
- {
- u8 input[32] = { 0 };
- u8 output[32];
- u8 flags;
- int ret;
- ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, input, sizeof(input), output,
- sizeof(output));
- if (ret < 0)
- return ret;
- flags = output[MSI_PLATFORM_EC_FLAGS_OFFSET];
- dev_dbg(&data->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(&data->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(&data->wdev->dev, "Loading on a non-Tigerlake platform\n");
- }
- return 0;
- }
- static int msi_wmi_platform_init(struct msi_wmi_platform_data *data)
- {
- u8 input[32] = { 0 };
- u8 output[32];
- int ret;
- ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, input, sizeof(input), output,
- sizeof(output));
- if (ret < 0)
- return ret;
- dev_dbg(&data->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(&data->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)
- {
- struct msi_wmi_platform_data *data;
- int ret;
- data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
- data->wdev = wdev;
- dev_set_drvdata(&wdev->dev, data);
- ret = devm_mutex_init(&wdev->dev, &data->wmi_lock);
- if (ret < 0)
- return ret;
- ret = msi_wmi_platform_init(data);
- if (ret < 0)
- return ret;
- ret = msi_wmi_platform_ec_init(data);
- if (ret < 0)
- return ret;
- msi_wmi_platform_debugfs_init(data);
- return msi_wmi_platform_hwmon_init(data);
- }
- 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,
- };
- /*
- * MSI reused the WMI GUID from the WMI-ACPI sample code provided by Microsoft,
- * so other manufacturers might use it as well for their WMI-ACPI implementations.
- */
- static const struct dmi_system_id msi_wmi_platform_whitelist[] __initconst = {
- {
- .matches = {
- DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"),
- },
- },
- {
- .matches = {
- DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"),
- },
- },
- { }
- };
- static int __init msi_wmi_platform_module_init(void)
- {
- if (!dmi_check_system(msi_wmi_platform_whitelist)) {
- if (!force)
- return -ENODEV;
- pr_warn("Ignoring DMI whitelist\n");
- }
- return wmi_driver_register(&msi_wmi_platform_driver);
- }
- static void __exit msi_wmi_platform_module_exit(void)
- {
- wmi_driver_unregister(&msi_wmi_platform_driver);
- }
- module_init(msi_wmi_platform_module_init);
- module_exit(msi_wmi_platform_module_exit);
- MODULE_AUTHOR("Armin Wolf <W_Armin@gmx.de>");
- MODULE_DESCRIPTION("MSI WMI platform features");
- MODULE_LICENSE("GPL");
|