File Index
Files and directories
Sign in
FILE

msi-wmi-platform.c

File preview with syntax highlighting.

linux-6.19.14/drivers/platform/x86/msi-wmi-platform.c C
  1// SPDX-License-Identifier: GPL-2.0-or-later
  2/*
  3 * Linux driver for WMI platform features on MSI notebooks.
  4 *
  5 * Copyright (C) 2024 Armin Wolf <W_Armin@gmx.de>
  6 */
  7
  8#define pr_format(fmt) KBUILD_MODNAME ": " fmt
  9
 10#include <linux/acpi.h>
 11#include <linux/bits.h>
 12#include <linux/bitfield.h>
 13#include <linux/cleanup.h>
 14#include <linux/debugfs.h>
 15#include <linux/device.h>
 16#include <linux/device/driver.h>
 17#include <linux/dmi.h>
 18#include <linux/errno.h>
 19#include <linux/hwmon.h>
 20#include <linux/kernel.h>
 21#include <linux/module.h>
 22#include <linux/mutex.h>
 23#include <linux/printk.h>
 24#include <linux/rwsem.h>
 25#include <linux/types.h>
 26#include <linux/wmi.h>
 27
 28#include <linux/unaligned.h>
 29
 30#define DRIVER_NAME	"msi-wmi-platform"
 31
 32#define MSI_PLATFORM_GUID	"ABBC0F6E-8EA1-11D1-00A0-C90629100000"
 33
 34#define MSI_WMI_PLATFORM_INTERFACE_VERSION	2
 35
 36#define MSI_PLATFORM_WMI_MAJOR_OFFSET	1
 37#define MSI_PLATFORM_WMI_MINOR_OFFSET	2
 38
 39#define MSI_PLATFORM_EC_FLAGS_OFFSET	1
 40#define MSI_PLATFORM_EC_MINOR_MASK	GENMASK(3, 0)
 41#define MSI_PLATFORM_EC_MAJOR_MASK	GENMASK(5, 4)
 42#define MSI_PLATFORM_EC_CHANGED_PAGE	BIT(6)
 43#define MSI_PLATFORM_EC_IS_TIGERLAKE	BIT(7)
 44#define MSI_PLATFORM_EC_VERSION_OFFSET	2
 45
 46static bool force;
 47module_param_unsafe(force, bool, 0);
 48MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions");
 49
 50enum msi_wmi_platform_method {
 51	MSI_PLATFORM_GET_PACKAGE	= 0x01,
 52	MSI_PLATFORM_SET_PACKAGE	= 0x02,
 53	MSI_PLATFORM_GET_EC		= 0x03,
 54	MSI_PLATFORM_SET_EC		= 0x04,
 55	MSI_PLATFORM_GET_BIOS		= 0x05,
 56	MSI_PLATFORM_SET_BIOS		= 0x06,
 57	MSI_PLATFORM_GET_SMBUS		= 0x07,
 58	MSI_PLATFORM_SET_SMBUS		= 0x08,
 59	MSI_PLATFORM_GET_MASTER_BATTERY = 0x09,
 60	MSI_PLATFORM_SET_MASTER_BATTERY = 0x0a,
 61	MSI_PLATFORM_GET_SLAVE_BATTERY	= 0x0b,
 62	MSI_PLATFORM_SET_SLAVE_BATTERY	= 0x0c,
 63	MSI_PLATFORM_GET_TEMPERATURE	= 0x0d,
 64	MSI_PLATFORM_SET_TEMPERATURE	= 0x0e,
 65	MSI_PLATFORM_GET_THERMAL	= 0x0f,
 66	MSI_PLATFORM_SET_THERMAL	= 0x10,
 67	MSI_PLATFORM_GET_FAN		= 0x11,
 68	MSI_PLATFORM_SET_FAN		= 0x12,
 69	MSI_PLATFORM_GET_DEVICE		= 0x13,
 70	MSI_PLATFORM_SET_DEVICE		= 0x14,
 71	MSI_PLATFORM_GET_POWER		= 0x15,
 72	MSI_PLATFORM_SET_POWER		= 0x16,
 73	MSI_PLATFORM_GET_DEBUG		= 0x17,
 74	MSI_PLATFORM_SET_DEBUG		= 0x18,
 75	MSI_PLATFORM_GET_AP		= 0x19,
 76	MSI_PLATFORM_SET_AP		= 0x1a,
 77	MSI_PLATFORM_GET_DATA		= 0x1b,
 78	MSI_PLATFORM_SET_DATA		= 0x1c,
 79	MSI_PLATFORM_GET_WMI		= 0x1d,
 80};
 81
 82struct msi_wmi_platform_data {
 83	struct wmi_device *wdev;
 84	struct mutex wmi_lock;	/* Necessary when calling WMI methods */
 85};
 86
 87struct msi_wmi_platform_debugfs_data {
 88	struct msi_wmi_platform_data *data;
 89	enum msi_wmi_platform_method method;
 90	struct rw_semaphore buffer_lock;	/* Protects debugfs buffer */
 91	size_t length;
 92	u8 buffer[32];
 93};
 94
 95static const char * const msi_wmi_platform_debugfs_names[] = {
 96	"get_package",
 97	"set_package",
 98	"get_ec",
 99	"set_ec",
100	"get_bios",
101	"set_bios",
102	"get_smbus",
103	"set_smbus",
104	"get_master_battery",
105	"set_master_battery",
106	"get_slave_battery",
107	"set_slave_battery",
108	"get_temperature",
109	"set_temperature",
110	"get_thermal",
111	"set_thermal",
112	"get_fan",
113	"set_fan",
114	"get_device",
115	"set_device",
116	"get_power",
117	"set_power",
118	"get_debug",
119	"set_debug",
120	"get_ap",
121	"set_ap",
122	"get_data",
123	"set_data",
124	"get_wmi"
125};
126
127static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, size_t length)
128{
129	if (obj->type != ACPI_TYPE_BUFFER)
130		return -ENOMSG;
131
132	if (obj->buffer.length != length)
133		return -EPROTO;
134
135	if (!obj->buffer.pointer[0])
136		return -EIO;
137
138	memcpy(output, obj->buffer.pointer, obj->buffer.length);
139
140	return 0;
141}
142
143static int msi_wmi_platform_query(struct msi_wmi_platform_data *data,
144				  enum msi_wmi_platform_method method, u8 *input,
145				  size_t input_length, u8 *output, size_t output_length)
146{
147	struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
148	struct acpi_buffer in = {
149		.length = input_length,
150		.pointer = input
151	};
152	union acpi_object *obj;
153	acpi_status status;
154	int ret;
155
156	if (!input_length || !output_length)
157		return -EINVAL;
158
159	/*
160	 * The ACPI control method responsible for handling the WMI method calls
161	 * is not thread-safe. Because of this we have to do the locking ourself.
162	 */
163	scoped_guard(mutex, &data->wmi_lock) {
164		status = wmidev_evaluate_method(data->wdev, 0x0, method, &in, &out);
165		if (ACPI_FAILURE(status))
166			return -EIO;
167	}
168
169	obj = out.pointer;
170	if (!obj)
171		return -ENODATA;
172
173	ret = msi_wmi_platform_parse_buffer(obj, output, output_length);
174	kfree(obj);
175
176	return ret;
177}
178
179static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type,
180					   u32 attr, int channel)
181{
182	return 0444;
183}
184
185static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
186				 int channel, long *val)
187{
188	struct msi_wmi_platform_data *data = dev_get_drvdata(dev);
189	u8 input[32] = { 0 };
190	u8 output[32];
191	u16 value;
192	int ret;
193
194	ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, input, sizeof(input), output,
195				     sizeof(output));
196	if (ret < 0)
197		return ret;
198
199	value = get_unaligned_be16(&output[channel * 2 + 1]);
200	if (!value)
201		*val = 0;
202	else
203		*val = 480000 / value;
204
205	return 0;
206}
207
208static const struct hwmon_ops msi_wmi_platform_ops = {
209	.is_visible = msi_wmi_platform_is_visible,
210	.read = msi_wmi_platform_read,
211};
212
213static const struct hwmon_channel_info * const msi_wmi_platform_info[] = {
214	HWMON_CHANNEL_INFO(fan,
215			   HWMON_F_INPUT,
216			   HWMON_F_INPUT,
217			   HWMON_F_INPUT,
218			   HWMON_F_INPUT
219			   ),
220	NULL
221};
222
223static const struct hwmon_chip_info msi_wmi_platform_chip_info = {
224	.ops = &msi_wmi_platform_ops,
225	.info = msi_wmi_platform_info,
226};
227
228static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, size_t length,
229				      loff_t *offset)
230{
231	struct seq_file *seq = fp->private_data;
232	struct msi_wmi_platform_debugfs_data *data = seq->private;
233	u8 payload[32] = { };
234	ssize_t ret;
235
236	/* Do not allow partial writes */
237	if (*offset != 0)
238		return -EINVAL;
239
240	/* Do not allow incomplete command buffers */
241	if (length != data->length)
242		return -EINVAL;
243
244	ret = simple_write_to_buffer(payload, sizeof(payload), offset, input, length);
245	if (ret < 0)
246		return ret;
247
248	down_write(&data->buffer_lock);
249	ret = msi_wmi_platform_query(data->data, data->method, payload, data->length, data->buffer,
250				     data->length);
251	up_write(&data->buffer_lock);
252
253	if (ret < 0)
254		return ret;
255
256	return length;
257}
258
259static int msi_wmi_platform_show(struct seq_file *seq, void *p)
260{
261	struct msi_wmi_platform_debugfs_data *data = seq->private;
262	int ret;
263
264	down_read(&data->buffer_lock);
265	ret = seq_write(seq, data->buffer, data->length);
266	up_read(&data->buffer_lock);
267
268	return ret;
269}
270
271static int msi_wmi_platform_open(struct inode *inode, struct file *fp)
272{
273	struct msi_wmi_platform_debugfs_data *data = inode->i_private;
274
275	/* The seq_file uses the last byte of the buffer for detecting buffer overflows */
276	return single_open_size(fp, msi_wmi_platform_show, data, data->length + 1);
277}
278
279static const struct file_operations msi_wmi_platform_debugfs_fops = {
280	.owner = THIS_MODULE,
281	.open = msi_wmi_platform_open,
282	.read = seq_read,
283	.write = msi_wmi_platform_write,
284	.llseek = seq_lseek,
285	.release = single_release,
286};
287
288static void msi_wmi_platform_debugfs_remove(void *data)
289{
290	struct dentry *dir = data;
291
292	debugfs_remove_recursive(dir);
293}
294
295static void msi_wmi_platform_debugfs_add(struct msi_wmi_platform_data *drvdata, struct dentry *dir,
296					 const char *name, enum msi_wmi_platform_method method)
297{
298	struct msi_wmi_platform_debugfs_data *data;
299	struct dentry *entry;
300
301	data = devm_kzalloc(&drvdata->wdev->dev, sizeof(*data), GFP_KERNEL);
302	if (!data)
303		return;
304
305	data->data = drvdata;
306	data->method = method;
307	init_rwsem(&data->buffer_lock);
308
309	/* The ACPI firmware for now always requires a 32 byte input buffer due to
310	 * a peculiarity in how Windows handles the CreateByteField() ACPI operator.
311	 */
312	data->length = 32;
313
314	entry = debugfs_create_file(name, 0600, dir, data, &msi_wmi_platform_debugfs_fops);
315	if (IS_ERR(entry))
316		devm_kfree(&drvdata->wdev->dev, data);
317}
318
319static void msi_wmi_platform_debugfs_init(struct msi_wmi_platform_data *data)
320{
321	struct dentry *dir;
322	char dir_name[64];
323	int ret, method;
324
325	scnprintf(dir_name, ARRAY_SIZE(dir_name), "%s-%s", DRIVER_NAME, dev_name(&data->wdev->dev));
326
327	dir = debugfs_create_dir(dir_name, NULL);
328	if (IS_ERR(dir))
329		return;
330
331	ret = devm_add_action_or_reset(&data->wdev->dev, msi_wmi_platform_debugfs_remove, dir);
332	if (ret < 0)
333		return;
334
335	for (method = MSI_PLATFORM_GET_PACKAGE; method <= MSI_PLATFORM_GET_WMI; method++)
336		msi_wmi_platform_debugfs_add(data, dir, msi_wmi_platform_debugfs_names[method - 1],
337					     method);
338}
339
340static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data)
341{
342	struct device *hdev;
343
344	hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data,
345						    &msi_wmi_platform_chip_info, NULL);
346
347	return PTR_ERR_OR_ZERO(hdev);
348}
349
350static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data)
351{
352	u8 input[32] = { 0 };
353	u8 output[32];
354	u8 flags;
355	int ret;
356
357	ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, input, sizeof(input), output,
358				     sizeof(output));
359	if (ret < 0)
360		return ret;
361
362	flags = output[MSI_PLATFORM_EC_FLAGS_OFFSET];
363
364	dev_dbg(&data->wdev->dev, "EC RAM version %lu.%lu\n",
365		FIELD_GET(MSI_PLATFORM_EC_MAJOR_MASK, flags),
366		FIELD_GET(MSI_PLATFORM_EC_MINOR_MASK, flags));
367	dev_dbg(&data->wdev->dev, "EC firmware version %.28s\n",
368		&output[MSI_PLATFORM_EC_VERSION_OFFSET]);
369
370	if (!(flags & MSI_PLATFORM_EC_IS_TIGERLAKE)) {
371		if (!force)
372			return -ENODEV;
373
374		dev_warn(&data->wdev->dev, "Loading on a non-Tigerlake platform\n");
375	}
376
377	return 0;
378}
379
380static int msi_wmi_platform_init(struct msi_wmi_platform_data *data)
381{
382	u8 input[32] = { 0 };
383	u8 output[32];
384	int ret;
385
386	ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, input, sizeof(input), output,
387				     sizeof(output));
388	if (ret < 0)
389		return ret;
390
391	dev_dbg(&data->wdev->dev, "WMI interface version %u.%u\n",
392		output[MSI_PLATFORM_WMI_MAJOR_OFFSET],
393		output[MSI_PLATFORM_WMI_MINOR_OFFSET]);
394
395	if (output[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) {
396		if (!force)
397			return -ENODEV;
398
399		dev_warn(&data->wdev->dev,
400			 "Loading despite unsupported WMI interface version (%u.%u)\n",
401			 output[MSI_PLATFORM_WMI_MAJOR_OFFSET],
402			 output[MSI_PLATFORM_WMI_MINOR_OFFSET]);
403	}
404
405	return 0;
406}
407
408static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context)
409{
410	struct msi_wmi_platform_data *data;
411	int ret;
412
413	data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL);
414	if (!data)
415		return -ENOMEM;
416
417	data->wdev = wdev;
418	dev_set_drvdata(&wdev->dev, data);
419
420	ret = devm_mutex_init(&wdev->dev, &data->wmi_lock);
421	if (ret < 0)
422		return ret;
423
424	ret = msi_wmi_platform_init(data);
425	if (ret < 0)
426		return ret;
427
428	ret = msi_wmi_platform_ec_init(data);
429	if (ret < 0)
430		return ret;
431
432	msi_wmi_platform_debugfs_init(data);
433
434	return msi_wmi_platform_hwmon_init(data);
435}
436
437static const struct wmi_device_id msi_wmi_platform_id_table[] = {
438	{ MSI_PLATFORM_GUID, NULL },
439	{ }
440};
441MODULE_DEVICE_TABLE(wmi, msi_wmi_platform_id_table);
442
443static struct wmi_driver msi_wmi_platform_driver = {
444	.driver = {
445		.name = DRIVER_NAME,
446		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
447	},
448	.id_table = msi_wmi_platform_id_table,
449	.probe = msi_wmi_platform_probe,
450	.no_singleton = true,
451};
452
453/*
454 * MSI reused the WMI GUID from the WMI-ACPI sample code provided by Microsoft,
455 * so other manufacturers might use it as well for their WMI-ACPI implementations.
456 */
457static const struct dmi_system_id msi_wmi_platform_whitelist[] __initconst = {
458	{
459		.matches = {
460			DMI_MATCH(DMI_SYS_VENDOR, "MICRO-STAR INT"),
461		},
462	},
463	{
464		.matches = {
465			DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International"),
466		},
467	},
468	{ }
469};
470
471static int __init msi_wmi_platform_module_init(void)
472{
473	if (!dmi_check_system(msi_wmi_platform_whitelist)) {
474		if (!force)
475			return -ENODEV;
476
477		pr_warn("Ignoring DMI whitelist\n");
478	}
479
480	return wmi_driver_register(&msi_wmi_platform_driver);
481}
482
483static void __exit msi_wmi_platform_module_exit(void)
484{
485	wmi_driver_unregister(&msi_wmi_platform_driver);
486}
487
488module_init(msi_wmi_platform_module_init);
489module_exit(msi_wmi_platform_module_exit);
490
491
492MODULE_AUTHOR("Armin Wolf <W_Armin@gmx.de>");
493MODULE_DESCRIPTION("MSI WMI platform features");
494MODULE_LICENSE("GPL");
~/ Read-only file preview.