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