1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * fan_core.c - ACPI Fan core Driver
4 *
5 * Copyright (C) 2001, 2002 Andy Grover <andrew.grover@intel.com>
6 * Copyright (C) 2001, 2002 Paul Diefenbaugh <paul.s.diefenbaugh@intel.com>
7 * Copyright (C) 2022 Intel Corporation. All rights reserved.
8 */
9
10 #include <linux/bits.h>
11 #include <linux/kernel.h>
12 #include <linux/limits.h>
13 #include <linux/math.h>
14 #include <linux/math64.h>
15 #include <linux/module.h>
16 #include <linux/init.h>
17 #include <linux/types.h>
18 #include <linux/uaccess.h>
19 #include <linux/uuid.h>
20 #include <linux/thermal.h>
21 #include <linux/acpi.h>
22 #include <linux/platform_device.h>
23 #include <linux/sort.h>
24
25 #include "fan.h"
26
27 #define ACPI_FAN_NOTIFY_STATE_CHANGED 0x80
28
29 /*
30 * Defined inside the "Fan Noise Signal" section at
31 * https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/design-guide.
32 */
33 static const guid_t acpi_fan_microsoft_guid = GUID_INIT(0xA7611840, 0x99FE, 0x41AE, 0xA4, 0x88,
34 0x35, 0xC7, 0x59, 0x26, 0xC8, 0xEB);
35 #define ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY 1
36 #define ACPI_FAN_DSM_SET_TRIP_POINTS 2
37 #define ACPI_FAN_DSM_GET_OPERATING_RANGES 3
38
39 /*
40 * Ensures that fans with a very low trip point granularity
41 * do not send too many notifications.
42 */
43 static uint min_trip_distance = 100;
44 module_param(min_trip_distance, uint, 0);
45 MODULE_PARM_DESC(min_trip_distance, "Minimum distance between fan speed trip points in RPM");
46
47 static const struct acpi_device_id fan_device_ids[] = {
48 ACPI_FAN_DEVICE_IDS,
49 {"", 0},
50 };
51 MODULE_DEVICE_TABLE(acpi, fan_device_ids);
52
53 /* thermal cooling device callbacks */
fan_get_max_state(struct thermal_cooling_device * cdev,unsigned long * state)54 static int fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long
55 *state)
56 {
57 struct acpi_device *device = cdev->devdata;
58 struct acpi_fan *fan = acpi_driver_data(device);
59
60 if (fan->acpi4) {
61 if (fan->fif.fine_grain_ctrl)
62 *state = 100 / fan->fif.step_size;
63 else
64 *state = fan->fps_count - 1;
65 } else {
66 *state = 1;
67 }
68
69 return 0;
70 }
71
acpi_fan_get_fst(acpi_handle handle,struct acpi_fan_fst * fst)72 int acpi_fan_get_fst(acpi_handle handle, struct acpi_fan_fst *fst)
73 {
74 struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
75 union acpi_object *obj;
76 acpi_status status;
77 int ret = 0;
78
79 status = acpi_evaluate_object(handle, "_FST", NULL, &buffer);
80 if (ACPI_FAILURE(status))
81 return -EIO;
82
83 obj = buffer.pointer;
84 if (!obj)
85 return -ENODATA;
86
87 if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count != 3) {
88 ret = -EPROTO;
89 goto err;
90 }
91
92 if (obj->package.elements[0].type != ACPI_TYPE_INTEGER ||
93 obj->package.elements[1].type != ACPI_TYPE_INTEGER ||
94 obj->package.elements[2].type != ACPI_TYPE_INTEGER) {
95 ret = -EPROTO;
96 goto err;
97 }
98
99 fst->revision = obj->package.elements[0].integer.value;
100 fst->control = obj->package.elements[1].integer.value;
101 fst->speed = obj->package.elements[2].integer.value;
102
103 err:
104 kfree(obj);
105 return ret;
106 }
107
fan_get_state_acpi4(struct acpi_device * device,unsigned long * state)108 static int fan_get_state_acpi4(struct acpi_device *device, unsigned long *state)
109 {
110 struct acpi_fan *fan = acpi_driver_data(device);
111 struct acpi_fan_fst fst;
112 int status, i;
113
114 status = acpi_fan_get_fst(device->handle, &fst);
115 if (status)
116 return status;
117
118 if (fan->fif.fine_grain_ctrl) {
119 /* This control should be same what we set using _FSL by spec */
120 if (fst.control > 100) {
121 dev_dbg(&device->dev, "Invalid control value returned\n");
122 goto match_fps;
123 }
124
125 *state = (int) fst.control / fan->fif.step_size;
126 return 0;
127 }
128
129 match_fps:
130 for (i = 0; i < fan->fps_count; i++) {
131 if (fst.control == fan->fps[i].control)
132 break;
133 }
134 if (i == fan->fps_count) {
135 dev_dbg(&device->dev, "No matching fps control value\n");
136 return -EINVAL;
137 }
138
139 *state = i;
140
141 return status;
142 }
143
fan_get_state(struct acpi_device * device,unsigned long * state)144 static int fan_get_state(struct acpi_device *device, unsigned long *state)
145 {
146 int result;
147 int acpi_state = ACPI_STATE_D0;
148
149 result = acpi_device_update_power(device, &acpi_state);
150 if (result)
151 return result;
152
153 *state = acpi_state == ACPI_STATE_D3_COLD
154 || acpi_state == ACPI_STATE_D3_HOT ?
155 0 : (acpi_state == ACPI_STATE_D0 ? 1 : -1);
156 return 0;
157 }
158
fan_get_cur_state(struct thermal_cooling_device * cdev,unsigned long * state)159 static int fan_get_cur_state(struct thermal_cooling_device *cdev, unsigned long
160 *state)
161 {
162 struct acpi_device *device = cdev->devdata;
163 struct acpi_fan *fan = acpi_driver_data(device);
164
165 if (fan->acpi4)
166 return fan_get_state_acpi4(device, state);
167 else
168 return fan_get_state(device, state);
169 }
170
fan_set_state(struct acpi_device * device,unsigned long state)171 static int fan_set_state(struct acpi_device *device, unsigned long state)
172 {
173 if (state != 0 && state != 1)
174 return -EINVAL;
175
176 return acpi_device_set_power(device,
177 state ? ACPI_STATE_D0 : ACPI_STATE_D3_COLD);
178 }
179
fan_set_state_acpi4(struct acpi_device * device,unsigned long state)180 static int fan_set_state_acpi4(struct acpi_device *device, unsigned long state)
181 {
182 struct acpi_fan *fan = acpi_driver_data(device);
183 acpi_status status;
184 u64 value = state;
185 int max_state;
186
187 if (fan->fif.fine_grain_ctrl)
188 max_state = 100 / fan->fif.step_size;
189 else
190 max_state = fan->fps_count - 1;
191
192 if (state > max_state)
193 return -EINVAL;
194
195 if (fan->fif.fine_grain_ctrl) {
196 value *= fan->fif.step_size;
197 /* Spec allows compensate the last step only */
198 if (value + fan->fif.step_size > 100)
199 value = 100;
200 } else {
201 value = fan->fps[state].control;
202 }
203
204 status = acpi_execute_simple_method(device->handle, "_FSL", value);
205 if (ACPI_FAILURE(status)) {
206 dev_dbg(&device->dev, "Failed to set state by _FSL\n");
207 return -ENODEV;
208 }
209
210 return 0;
211 }
212
213 static int
fan_set_cur_state(struct thermal_cooling_device * cdev,unsigned long state)214 fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state)
215 {
216 struct acpi_device *device = cdev->devdata;
217 struct acpi_fan *fan = acpi_driver_data(device);
218
219 if (fan->acpi4)
220 return fan_set_state_acpi4(device, state);
221 else
222 return fan_set_state(device, state);
223 }
224
225 static const struct thermal_cooling_device_ops fan_cooling_ops = {
226 .get_max_state = fan_get_max_state,
227 .get_cur_state = fan_get_cur_state,
228 .set_cur_state = fan_set_cur_state,
229 };
230
231 /* --------------------------------------------------------------------------
232 * Driver Interface
233 * --------------------------------------------------------------------------
234 */
235
acpi_fan_get_fif(struct acpi_device * device)236 static int acpi_fan_get_fif(struct acpi_device *device)
237 {
238 struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
239 struct acpi_fan *fan = acpi_driver_data(device);
240 struct acpi_buffer format = { sizeof("NNNN"), "NNNN" };
241 u64 fields[4];
242 struct acpi_buffer fif = { sizeof(fields), fields };
243 union acpi_object *obj;
244 acpi_status status;
245
246 status = acpi_evaluate_object(device->handle, "_FIF", NULL, &buffer);
247 if (ACPI_FAILURE(status))
248 return status;
249
250 obj = buffer.pointer;
251 if (!obj || obj->type != ACPI_TYPE_PACKAGE) {
252 dev_err(&device->dev, "Invalid _FIF data\n");
253 status = -EINVAL;
254 goto err;
255 }
256
257 status = acpi_extract_package(obj, &format, &fif);
258 if (ACPI_FAILURE(status)) {
259 dev_err(&device->dev, "Invalid _FIF element\n");
260 status = -EINVAL;
261 goto err;
262 }
263
264 fan->fif.revision = fields[0];
265 fan->fif.fine_grain_ctrl = fields[1];
266 fan->fif.step_size = fields[2];
267 fan->fif.low_speed_notification = fields[3];
268
269 /* If there is a bug in step size and set as 0, change to 1 */
270 if (!fan->fif.step_size)
271 fan->fif.step_size = 1;
272 /* If step size > 9, change to 9 (by spec valid values 1-9) */
273 else if (fan->fif.step_size > 9)
274 fan->fif.step_size = 9;
275 err:
276 kfree(obj);
277 return status;
278 }
279
acpi_fan_speed_cmp(const void * a,const void * b)280 static int acpi_fan_speed_cmp(const void *a, const void *b)
281 {
282 const struct acpi_fan_fps *fps1 = a;
283 const struct acpi_fan_fps *fps2 = b;
284 return fps1->speed - fps2->speed;
285 }
286
acpi_fan_get_fps(struct acpi_device * device)287 static int acpi_fan_get_fps(struct acpi_device *device)
288 {
289 struct acpi_fan *fan = acpi_driver_data(device);
290 struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
291 union acpi_object *obj;
292 acpi_status status;
293 int i;
294
295 status = acpi_evaluate_object(device->handle, "_FPS", NULL, &buffer);
296 if (ACPI_FAILURE(status))
297 return status;
298
299 obj = buffer.pointer;
300 if (!obj || obj->type != ACPI_TYPE_PACKAGE || obj->package.count < 2) {
301 dev_err(&device->dev, "Invalid _FPS data\n");
302 status = -EINVAL;
303 goto err;
304 }
305
306 fan->fps_count = obj->package.count - 1; /* minus revision field */
307 fan->fps = devm_kcalloc(&device->dev,
308 fan->fps_count, sizeof(struct acpi_fan_fps),
309 GFP_KERNEL);
310 if (!fan->fps) {
311 dev_err(&device->dev, "Not enough memory\n");
312 status = -ENOMEM;
313 goto err;
314 }
315 for (i = 0; i < fan->fps_count; i++) {
316 struct acpi_buffer format = { sizeof("NNNNN"), "NNNNN" };
317 struct acpi_buffer fps = { offsetof(struct acpi_fan_fps, name),
318 &fan->fps[i] };
319 status = acpi_extract_package(&obj->package.elements[i + 1],
320 &format, &fps);
321 if (ACPI_FAILURE(status)) {
322 dev_err(&device->dev, "Invalid _FPS element\n");
323 goto err;
324 }
325 }
326
327 /* sort the state array according to fan speed in increase order */
328 sort(fan->fps, fan->fps_count, sizeof(*fan->fps),
329 acpi_fan_speed_cmp, NULL);
330
331 err:
332 kfree(obj);
333 return status;
334 }
335
acpi_fan_dsm_init(struct device * dev)336 static int acpi_fan_dsm_init(struct device *dev)
337 {
338 union acpi_object dummy = {
339 .package = {
340 .type = ACPI_TYPE_PACKAGE,
341 .count = 0,
342 .elements = NULL,
343 },
344 };
345 struct acpi_fan *fan = dev_get_drvdata(dev);
346 union acpi_object *obj;
347 int ret = 0;
348
349 if (!acpi_check_dsm(fan->handle, &acpi_fan_microsoft_guid, 0,
350 BIT(ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY) |
351 BIT(ACPI_FAN_DSM_SET_TRIP_POINTS)))
352 return 0;
353
354 dev_info(dev, "Using Microsoft fan extensions\n");
355
356 obj = acpi_evaluate_dsm_typed(fan->handle, &acpi_fan_microsoft_guid, 0,
357 ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY, &dummy,
358 ACPI_TYPE_INTEGER);
359 if (!obj)
360 return -EIO;
361
362 if (obj->integer.value > U32_MAX)
363 ret = -EOVERFLOW;
364 else
365 fan->fan_trip_granularity = obj->integer.value;
366
367 kfree(obj);
368
369 return ret;
370 }
371
acpi_fan_dsm_set_trip_points(struct device * dev,u64 upper,u64 lower)372 static int acpi_fan_dsm_set_trip_points(struct device *dev, u64 upper, u64 lower)
373 {
374 union acpi_object args[2] = {
375 {
376 .integer = {
377 .type = ACPI_TYPE_INTEGER,
378 .value = lower,
379 },
380 },
381 {
382 .integer = {
383 .type = ACPI_TYPE_INTEGER,
384 .value = upper,
385 },
386 },
387 };
388 struct acpi_fan *fan = dev_get_drvdata(dev);
389 union acpi_object in = {
390 .package = {
391 .type = ACPI_TYPE_PACKAGE,
392 .count = ARRAY_SIZE(args),
393 .elements = args,
394 },
395 };
396 union acpi_object *obj;
397
398 obj = acpi_evaluate_dsm(fan->handle, &acpi_fan_microsoft_guid, 0,
399 ACPI_FAN_DSM_SET_TRIP_POINTS, &in);
400 kfree(obj);
401
402 return 0;
403 }
404
acpi_fan_dsm_start(struct device * dev)405 static int acpi_fan_dsm_start(struct device *dev)
406 {
407 struct acpi_fan *fan = dev_get_drvdata(dev);
408 int ret;
409
410 if (!fan->fan_trip_granularity)
411 return 0;
412
413 /*
414 * Some firmware implementations only update the values returned by the
415 * _FST control method when a notification is received. This usually
416 * works with Microsoft Windows as setting up trip points will keep
417 * triggering said notifications, but will cause issues when using _FST
418 * without the Microsoft-specific trip point extension.
419 *
420 * Because of this, an initial notification needs to be triggered to
421 * start the cycle of trip points updates. This is achieved by setting
422 * the trip points sequencially to two separate ranges. As by the
423 * Microsoft specification the firmware should trigger a notification
424 * immediately if the fan speed is outside the trip point range. This
425 * _should_ result in at least one notification as both ranges do not
426 * overlap, meaning that the current fan speed needs to be outside at
427 * least one range.
428 */
429 ret = acpi_fan_dsm_set_trip_points(dev, fan->fan_trip_granularity, 0);
430 if (ret < 0)
431 return ret;
432
433 return acpi_fan_dsm_set_trip_points(dev, fan->fan_trip_granularity * 3,
434 fan->fan_trip_granularity * 2);
435 }
436
acpi_fan_dsm_update_trips_points(struct device * dev,struct acpi_fan_fst * fst)437 static int acpi_fan_dsm_update_trips_points(struct device *dev, struct acpi_fan_fst *fst)
438 {
439 struct acpi_fan *fan = dev_get_drvdata(dev);
440 u64 upper, lower;
441
442 if (!fan->fan_trip_granularity)
443 return 0;
444
445 if (!acpi_fan_speed_valid(fst->speed))
446 return -EINVAL;
447
448 upper = roundup_u64(fst->speed + min_trip_distance, fan->fan_trip_granularity);
449 if (fst->speed <= min_trip_distance) {
450 lower = 0;
451 } else {
452 /*
453 * Valid fan speed values cannot be larger than 32 bit, so
454 * we can safely assume that no overflow will happen here.
455 */
456 lower = rounddown((u32)fst->speed - min_trip_distance, fan->fan_trip_granularity);
457 }
458
459 return acpi_fan_dsm_set_trip_points(dev, upper, lower);
460 }
461
acpi_fan_notify_handler(acpi_handle handle,u32 event,void * context)462 static void acpi_fan_notify_handler(acpi_handle handle, u32 event, void *context)
463 {
464 struct device *dev = context;
465 struct acpi_fan_fst fst;
466 int ret;
467
468 switch (event) {
469 case ACPI_FAN_NOTIFY_STATE_CHANGED:
470 /*
471 * The ACPI specification says that we must evaluate _FST when we
472 * receive an ACPI event indicating that the fan state has changed.
473 */
474 ret = acpi_fan_get_fst(handle, &fst);
475 if (ret < 0) {
476 dev_err(dev, "Error retrieving current fan status: %d\n", ret);
477 } else {
478 ret = acpi_fan_dsm_update_trips_points(dev, &fst);
479 if (ret < 0)
480 dev_err(dev, "Failed to update trip points: %d\n", ret);
481 }
482
483 acpi_fan_notify_hwmon(dev);
484 acpi_bus_generate_netlink_event("fan", dev_name(dev), event, 0);
485 break;
486 default:
487 dev_dbg(dev, "Unsupported ACPI notification 0x%x\n", event);
488 break;
489 }
490 }
491
acpi_fan_notify_remove(void * data)492 static void acpi_fan_notify_remove(void *data)
493 {
494 struct acpi_fan *fan = data;
495
496 acpi_remove_notify_handler(fan->handle, ACPI_DEVICE_NOTIFY, acpi_fan_notify_handler);
497 }
498
devm_acpi_fan_notify_init(struct device * dev)499 static int devm_acpi_fan_notify_init(struct device *dev)
500 {
501 struct acpi_fan *fan = dev_get_drvdata(dev);
502 acpi_status status;
503
504 status = acpi_install_notify_handler(fan->handle, ACPI_DEVICE_NOTIFY,
505 acpi_fan_notify_handler, dev);
506 if (ACPI_FAILURE(status))
507 return -EIO;
508
509 return devm_add_action_or_reset(dev, acpi_fan_notify_remove, fan);
510 }
511
acpi_fan_probe(struct platform_device * pdev)512 static int acpi_fan_probe(struct platform_device *pdev)
513 {
514 int result = 0;
515 struct thermal_cooling_device *cdev;
516 struct acpi_fan *fan;
517 struct acpi_device *device = ACPI_COMPANION(&pdev->dev);
518 char *name;
519
520 if (!device)
521 return -ENODEV;
522
523 fan = devm_kzalloc(&pdev->dev, sizeof(*fan), GFP_KERNEL);
524 if (!fan) {
525 dev_err(&device->dev, "No memory for fan\n");
526 return -ENOMEM;
527 }
528
529 fan->handle = device->handle;
530 device->driver_data = fan;
531 platform_set_drvdata(pdev, fan);
532
533 if (acpi_has_method(device->handle, "_FST")) {
534 fan->has_fst = true;
535 fan->acpi4 = acpi_has_method(device->handle, "_FIF") &&
536 acpi_has_method(device->handle, "_FPS") &&
537 acpi_has_method(device->handle, "_FSL");
538 }
539
540 if (fan->acpi4) {
541 result = acpi_fan_get_fif(device);
542 if (result)
543 return result;
544
545 result = acpi_fan_get_fps(device);
546 if (result)
547 return result;
548 }
549
550 if (fan->has_fst) {
551 result = acpi_fan_dsm_init(&pdev->dev);
552 if (result)
553 return result;
554
555 result = devm_acpi_fan_create_hwmon(&pdev->dev);
556 if (result)
557 return result;
558
559 result = devm_acpi_fan_notify_init(&pdev->dev);
560 if (result)
561 return result;
562
563 result = acpi_fan_dsm_start(&pdev->dev);
564 if (result) {
565 dev_err(&pdev->dev, "Failed to start Microsoft fan extensions\n");
566 return result;
567 }
568
569 result = acpi_fan_create_attributes(device);
570 if (result)
571 return result;
572 }
573
574 if (!fan->acpi4) {
575 result = acpi_device_update_power(device, NULL);
576 if (result) {
577 dev_err(&device->dev, "Failed to set initial power state\n");
578 goto err_end;
579 }
580 }
581
582 if (!strncmp(pdev->name, "PNP0C0B", strlen("PNP0C0B")))
583 name = "Fan";
584 else
585 name = acpi_device_bid(device);
586
587 cdev = thermal_cooling_device_register(name, device,
588 &fan_cooling_ops);
589 if (IS_ERR(cdev)) {
590 result = PTR_ERR(cdev);
591 goto err_end;
592 }
593
594 dev_dbg(&pdev->dev, "registered as cooling_device%d\n", cdev->id);
595
596 fan->cdev = cdev;
597 result = sysfs_create_link(&pdev->dev.kobj,
598 &cdev->device.kobj,
599 "thermal_cooling");
600 if (result) {
601 dev_err(&pdev->dev, "Failed to create sysfs link 'thermal_cooling'\n");
602 goto err_unregister;
603 }
604
605 result = sysfs_create_link(&cdev->device.kobj,
606 &pdev->dev.kobj,
607 "device");
608 if (result) {
609 dev_err(&pdev->dev, "Failed to create sysfs link 'device'\n");
610 goto err_remove_link;
611 }
612
613 return 0;
614
615 err_remove_link:
616 sysfs_remove_link(&pdev->dev.kobj, "thermal_cooling");
617 err_unregister:
618 thermal_cooling_device_unregister(cdev);
619 err_end:
620 if (fan->has_fst)
621 acpi_fan_delete_attributes(device);
622
623 return result;
624 }
625
acpi_fan_remove(struct platform_device * pdev)626 static void acpi_fan_remove(struct platform_device *pdev)
627 {
628 struct acpi_fan *fan = platform_get_drvdata(pdev);
629
630 if (fan->has_fst) {
631 struct acpi_device *device = ACPI_COMPANION(&pdev->dev);
632
633 acpi_fan_delete_attributes(device);
634 }
635 sysfs_remove_link(&pdev->dev.kobj, "thermal_cooling");
636 sysfs_remove_link(&fan->cdev->device.kobj, "device");
637 thermal_cooling_device_unregister(fan->cdev);
638 }
639
640 #ifdef CONFIG_PM_SLEEP
acpi_fan_suspend(struct device * dev)641 static int acpi_fan_suspend(struct device *dev)
642 {
643 struct acpi_fan *fan = dev_get_drvdata(dev);
644 if (fan->acpi4)
645 return 0;
646
647 acpi_device_set_power(ACPI_COMPANION(dev), ACPI_STATE_D0);
648
649 return AE_OK;
650 }
651
acpi_fan_resume(struct device * dev)652 static int acpi_fan_resume(struct device *dev)
653 {
654 struct acpi_fan *fan = dev_get_drvdata(dev);
655 int result;
656
657 if (fan->has_fst) {
658 result = acpi_fan_dsm_start(dev);
659 if (result)
660 dev_err(dev, "Failed to start Microsoft fan extensions: %d\n", result);
661 }
662
663 if (fan->acpi4)
664 return 0;
665
666 result = acpi_device_update_power(ACPI_COMPANION(dev), NULL);
667 if (result)
668 dev_err(dev, "Error updating fan power state\n");
669
670 return result;
671 }
672
673 static const struct dev_pm_ops acpi_fan_pm = {
674 .resume = acpi_fan_resume,
675 .freeze = acpi_fan_suspend,
676 .thaw = acpi_fan_resume,
677 .restore = acpi_fan_resume,
678 };
679 #define FAN_PM_OPS_PTR (&acpi_fan_pm)
680
681 #else
682
683 #define FAN_PM_OPS_PTR NULL
684
685 #endif
686
687 static struct platform_driver acpi_fan_driver = {
688 .probe = acpi_fan_probe,
689 .remove = acpi_fan_remove,
690 .driver = {
691 .name = "acpi-fan",
692 .acpi_match_table = fan_device_ids,
693 .pm = FAN_PM_OPS_PTR,
694 },
695 };
696
697 module_platform_driver(acpi_fan_driver);
698
699 MODULE_AUTHOR("Paul Diefenbaugh");
700 MODULE_DESCRIPTION("ACPI Fan Driver");
701 MODULE_LICENSE("GPL");
702