1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* acpi_thermal_rel.c driver for exporting ACPI thermal relationship |
3 | * |
4 | * Copyright (c) 2014 Intel Corp |
5 | */ |
6 | |
7 | /* |
8 | * Two functionalities included: |
9 | * 1. Export _TRT, _ART, via misc device interface to the userspace. |
10 | * 2. Provide parsing result to kernel drivers |
11 | * |
12 | */ |
13 | #include <linux/init.h> |
14 | #include <linux/export.h> |
15 | #include <linux/module.h> |
16 | #include <linux/device.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/io.h> |
19 | #include <linux/acpi.h> |
20 | #include <linux/uaccess.h> |
21 | #include <linux/miscdevice.h> |
22 | #include <linux/fs.h> |
23 | #include "acpi_thermal_rel.h" |
24 | |
25 | static acpi_handle acpi_thermal_rel_handle; |
26 | static DEFINE_SPINLOCK(acpi_thermal_rel_chrdev_lock); |
27 | static int acpi_thermal_rel_chrdev_count; /* #times opened */ |
28 | static int acpi_thermal_rel_chrdev_exclu; /* already open exclusive? */ |
29 | |
30 | static int acpi_thermal_rel_open(struct inode *inode, struct file *file) |
31 | { |
32 | spin_lock(lock: &acpi_thermal_rel_chrdev_lock); |
33 | if (acpi_thermal_rel_chrdev_exclu || |
34 | (acpi_thermal_rel_chrdev_count && (file->f_flags & O_EXCL))) { |
35 | spin_unlock(lock: &acpi_thermal_rel_chrdev_lock); |
36 | return -EBUSY; |
37 | } |
38 | |
39 | if (file->f_flags & O_EXCL) |
40 | acpi_thermal_rel_chrdev_exclu = 1; |
41 | acpi_thermal_rel_chrdev_count++; |
42 | |
43 | spin_unlock(lock: &acpi_thermal_rel_chrdev_lock); |
44 | |
45 | return nonseekable_open(inode, filp: file); |
46 | } |
47 | |
48 | static int acpi_thermal_rel_release(struct inode *inode, struct file *file) |
49 | { |
50 | spin_lock(lock: &acpi_thermal_rel_chrdev_lock); |
51 | acpi_thermal_rel_chrdev_count--; |
52 | acpi_thermal_rel_chrdev_exclu = 0; |
53 | spin_unlock(lock: &acpi_thermal_rel_chrdev_lock); |
54 | |
55 | return 0; |
56 | } |
57 | |
58 | /** |
59 | * acpi_parse_trt - Thermal Relationship Table _TRT for passive cooling |
60 | * |
61 | * @handle: ACPI handle of the device contains _TRT |
62 | * @trt_count: the number of valid entries resulted from parsing _TRT |
63 | * @trtp: pointer to pointer of array of _TRT entries in parsing result |
64 | * @create_dev: whether to create platform devices for target and source |
65 | * |
66 | */ |
67 | int acpi_parse_trt(acpi_handle handle, int *trt_count, struct trt **trtp, |
68 | bool create_dev) |
69 | { |
70 | acpi_status status; |
71 | int result = 0; |
72 | int i; |
73 | int nr_bad_entries = 0; |
74 | struct trt *trts; |
75 | union acpi_object *p; |
76 | struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; |
77 | struct acpi_buffer element = { 0, NULL }; |
78 | struct acpi_buffer trt_format = { sizeof("RRNNNNNN" ), "RRNNNNNN" }; |
79 | |
80 | status = acpi_evaluate_object(object: handle, pathname: "_TRT" , NULL, return_object_buffer: &buffer); |
81 | if (ACPI_FAILURE(status)) |
82 | return -ENODEV; |
83 | |
84 | p = buffer.pointer; |
85 | if (!p || (p->type != ACPI_TYPE_PACKAGE)) { |
86 | pr_err("Invalid _TRT data\n" ); |
87 | result = -EFAULT; |
88 | goto end; |
89 | } |
90 | |
91 | *trt_count = p->package.count; |
92 | trts = kcalloc(n: *trt_count, size: sizeof(struct trt), GFP_KERNEL); |
93 | if (!trts) { |
94 | result = -ENOMEM; |
95 | goto end; |
96 | } |
97 | |
98 | for (i = 0; i < *trt_count; i++) { |
99 | struct trt *trt = &trts[i - nr_bad_entries]; |
100 | |
101 | element.length = sizeof(struct trt); |
102 | element.pointer = trt; |
103 | |
104 | status = acpi_extract_package(package: &(p->package.elements[i]), |
105 | format: &trt_format, buffer: &element); |
106 | if (ACPI_FAILURE(status)) { |
107 | nr_bad_entries++; |
108 | pr_warn("_TRT package %d is invalid, ignored\n" , i); |
109 | continue; |
110 | } |
111 | if (!create_dev) |
112 | continue; |
113 | |
114 | if (!acpi_fetch_acpi_dev(handle: trt->source)) |
115 | pr_warn("Failed to get source ACPI device\n" ); |
116 | |
117 | if (!acpi_fetch_acpi_dev(handle: trt->target)) |
118 | pr_warn("Failed to get target ACPI device\n" ); |
119 | } |
120 | |
121 | result = 0; |
122 | |
123 | *trtp = trts; |
124 | /* don't count bad entries */ |
125 | *trt_count -= nr_bad_entries; |
126 | end: |
127 | kfree(objp: buffer.pointer); |
128 | return result; |
129 | } |
130 | EXPORT_SYMBOL(acpi_parse_trt); |
131 | |
132 | /** |
133 | * acpi_parse_art - Parse Active Relationship Table _ART |
134 | * |
135 | * @handle: ACPI handle of the device contains _ART |
136 | * @art_count: the number of valid entries resulted from parsing _ART |
137 | * @artp: pointer to pointer of array of art entries in parsing result |
138 | * @create_dev: whether to create platform devices for target and source |
139 | * |
140 | */ |
141 | int acpi_parse_art(acpi_handle handle, int *art_count, struct art **artp, |
142 | bool create_dev) |
143 | { |
144 | acpi_status status; |
145 | int result = 0; |
146 | int i; |
147 | int nr_bad_entries = 0; |
148 | struct art *arts; |
149 | union acpi_object *p; |
150 | struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; |
151 | struct acpi_buffer element = { 0, NULL }; |
152 | struct acpi_buffer art_format = { |
153 | sizeof("RRNNNNNNNNNNN" ), "RRNNNNNNNNNNN" }; |
154 | |
155 | status = acpi_evaluate_object(object: handle, pathname: "_ART" , NULL, return_object_buffer: &buffer); |
156 | if (ACPI_FAILURE(status)) |
157 | return -ENODEV; |
158 | |
159 | p = buffer.pointer; |
160 | if (!p || (p->type != ACPI_TYPE_PACKAGE)) { |
161 | pr_err("Invalid _ART data\n" ); |
162 | result = -EFAULT; |
163 | goto end; |
164 | } |
165 | |
166 | /* ignore p->package.elements[0], as this is _ART Revision field */ |
167 | *art_count = p->package.count - 1; |
168 | arts = kcalloc(n: *art_count, size: sizeof(struct art), GFP_KERNEL); |
169 | if (!arts) { |
170 | result = -ENOMEM; |
171 | goto end; |
172 | } |
173 | |
174 | for (i = 0; i < *art_count; i++) { |
175 | struct art *art = &arts[i - nr_bad_entries]; |
176 | |
177 | element.length = sizeof(struct art); |
178 | element.pointer = art; |
179 | |
180 | status = acpi_extract_package(package: &(p->package.elements[i + 1]), |
181 | format: &art_format, buffer: &element); |
182 | if (ACPI_FAILURE(status)) { |
183 | pr_warn("_ART package %d is invalid, ignored" , i); |
184 | nr_bad_entries++; |
185 | continue; |
186 | } |
187 | if (!create_dev) |
188 | continue; |
189 | |
190 | if (!acpi_fetch_acpi_dev(handle: art->source)) |
191 | pr_warn("Failed to get source ACPI device\n" ); |
192 | |
193 | if (!acpi_fetch_acpi_dev(handle: art->target)) |
194 | pr_warn("Failed to get target ACPI device\n" ); |
195 | } |
196 | |
197 | *artp = arts; |
198 | /* don't count bad entries */ |
199 | *art_count -= nr_bad_entries; |
200 | end: |
201 | kfree(objp: buffer.pointer); |
202 | return result; |
203 | } |
204 | EXPORT_SYMBOL(acpi_parse_art); |
205 | |
206 | /* |
207 | * acpi_parse_psvt - Passive Table (PSVT) for passive cooling |
208 | * |
209 | * @handle: ACPI handle of the device which contains PSVT |
210 | * @psvt_count: the number of valid entries resulted from parsing PSVT |
211 | * @psvtp: pointer to array of psvt entries |
212 | * |
213 | */ |
214 | static int acpi_parse_psvt(acpi_handle handle, int *psvt_count, struct psvt **psvtp) |
215 | { |
216 | struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; |
217 | int nr_bad_entries = 0, revision = 0; |
218 | union acpi_object *p; |
219 | acpi_status status; |
220 | int i, result = 0; |
221 | struct psvt *psvts; |
222 | |
223 | if (!acpi_has_method(handle, name: "PSVT" )) |
224 | return -ENODEV; |
225 | |
226 | status = acpi_evaluate_object(object: handle, pathname: "PSVT" , NULL, return_object_buffer: &buffer); |
227 | if (ACPI_FAILURE(status)) |
228 | return -ENODEV; |
229 | |
230 | p = buffer.pointer; |
231 | if (!p || (p->type != ACPI_TYPE_PACKAGE)) { |
232 | result = -EFAULT; |
233 | goto end; |
234 | } |
235 | |
236 | /* first package is the revision number */ |
237 | if (p->package.count > 0) { |
238 | union acpi_object *prev = &(p->package.elements[0]); |
239 | |
240 | if (prev->type == ACPI_TYPE_INTEGER) |
241 | revision = (int)prev->integer.value; |
242 | } else { |
243 | result = -EFAULT; |
244 | goto end; |
245 | } |
246 | |
247 | /* Support only version 2 */ |
248 | if (revision != 2) { |
249 | result = -EFAULT; |
250 | goto end; |
251 | } |
252 | |
253 | *psvt_count = p->package.count - 1; |
254 | if (!*psvt_count) { |
255 | result = -EFAULT; |
256 | goto end; |
257 | } |
258 | |
259 | psvts = kcalloc(n: *psvt_count, size: sizeof(*psvts), GFP_KERNEL); |
260 | if (!psvts) { |
261 | result = -ENOMEM; |
262 | goto end; |
263 | } |
264 | |
265 | /* Start index is 1 because the first package is the revision number */ |
266 | for (i = 1; i < p->package.count; i++) { |
267 | struct acpi_buffer psvt_int_format = { sizeof("RRNNNNNNNNNN" ), "RRNNNNNNNNNN" }; |
268 | struct acpi_buffer psvt_str_format = { sizeof("RRNNNNNSNNNN" ), "RRNNNNNSNNNN" }; |
269 | union acpi_object *package = &(p->package.elements[i]); |
270 | struct psvt *psvt = &psvts[i - 1 - nr_bad_entries]; |
271 | struct acpi_buffer *psvt_format = &psvt_int_format; |
272 | struct acpi_buffer element = { 0, NULL }; |
273 | union acpi_object *knob; |
274 | struct acpi_device *res; |
275 | struct psvt *psvt_ptr; |
276 | |
277 | element.length = ACPI_ALLOCATE_BUFFER; |
278 | element.pointer = NULL; |
279 | |
280 | if (package->package.count >= ACPI_NR_PSVT_ELEMENTS) { |
281 | knob = &(package->package.elements[ACPI_PSVT_CONTROL_KNOB]); |
282 | } else { |
283 | nr_bad_entries++; |
284 | pr_info("PSVT package %d is invalid, ignored\n" , i); |
285 | continue; |
286 | } |
287 | |
288 | if (knob->type == ACPI_TYPE_STRING) { |
289 | psvt_format = &psvt_str_format; |
290 | if (knob->string.length > ACPI_LIMIT_STR_MAX_LEN - 1) { |
291 | pr_info("PSVT package %d limit string len exceeds max\n" , i); |
292 | knob->string.length = ACPI_LIMIT_STR_MAX_LEN - 1; |
293 | } |
294 | } |
295 | |
296 | status = acpi_extract_package(package: &(p->package.elements[i]), format: psvt_format, buffer: &element); |
297 | if (ACPI_FAILURE(status)) { |
298 | nr_bad_entries++; |
299 | pr_info("PSVT package %d is invalid, ignored\n" , i); |
300 | continue; |
301 | } |
302 | |
303 | psvt_ptr = (struct psvt *)element.pointer; |
304 | |
305 | memcpy(psvt, psvt_ptr, sizeof(*psvt)); |
306 | |
307 | /* The limit element can be string or U64 */ |
308 | psvt->control_knob_type = (u64)knob->type; |
309 | |
310 | if (knob->type == ACPI_TYPE_STRING) { |
311 | memset(&psvt->limit, 0, sizeof(u64)); |
312 | strncpy(p: psvt->limit.string, q: psvt_ptr->limit.str_ptr, size: knob->string.length); |
313 | } else { |
314 | psvt->limit.integer = psvt_ptr->limit.integer; |
315 | } |
316 | |
317 | kfree(objp: element.pointer); |
318 | |
319 | res = acpi_fetch_acpi_dev(handle: psvt->source); |
320 | if (!res) { |
321 | nr_bad_entries++; |
322 | pr_info("Failed to get source ACPI device\n" ); |
323 | continue; |
324 | } |
325 | |
326 | res = acpi_fetch_acpi_dev(handle: psvt->target); |
327 | if (!res) { |
328 | nr_bad_entries++; |
329 | pr_info("Failed to get target ACPI device\n" ); |
330 | continue; |
331 | } |
332 | } |
333 | |
334 | /* don't count bad entries */ |
335 | *psvt_count -= nr_bad_entries; |
336 | |
337 | if (!*psvt_count) { |
338 | result = -EFAULT; |
339 | kfree(objp: psvts); |
340 | goto end; |
341 | } |
342 | |
343 | *psvtp = psvts; |
344 | |
345 | return 0; |
346 | |
347 | end: |
348 | kfree(objp: buffer.pointer); |
349 | return result; |
350 | } |
351 | |
352 | /* get device name from acpi handle */ |
353 | static void get_single_name(acpi_handle handle, char *name) |
354 | { |
355 | struct acpi_buffer buffer = {ACPI_ALLOCATE_BUFFER}; |
356 | |
357 | if (ACPI_FAILURE(acpi_get_name(handle, ACPI_SINGLE_NAME, &buffer))) |
358 | pr_warn("Failed to get device name from acpi handle\n" ); |
359 | else { |
360 | memcpy(name, buffer.pointer, ACPI_NAMESEG_SIZE); |
361 | kfree(objp: buffer.pointer); |
362 | } |
363 | } |
364 | |
365 | static int fill_art(char __user *ubuf) |
366 | { |
367 | int i; |
368 | int ret; |
369 | int count; |
370 | int art_len; |
371 | struct art *arts = NULL; |
372 | union art_object *art_user; |
373 | |
374 | ret = acpi_parse_art(acpi_thermal_rel_handle, &count, &arts, false); |
375 | if (ret) |
376 | goto free_art; |
377 | art_len = count * sizeof(union art_object); |
378 | art_user = kzalloc(size: art_len, GFP_KERNEL); |
379 | if (!art_user) { |
380 | ret = -ENOMEM; |
381 | goto free_art; |
382 | } |
383 | /* now fill in user art data */ |
384 | for (i = 0; i < count; i++) { |
385 | /* userspace art needs device name instead of acpi reference */ |
386 | get_single_name(handle: arts[i].source, name: art_user[i].source_device); |
387 | get_single_name(handle: arts[i].target, name: art_user[i].target_device); |
388 | /* copy the rest int data in addition to source and target */ |
389 | BUILD_BUG_ON(sizeof(art_user[i].data) != |
390 | sizeof(u64) * (ACPI_NR_ART_ELEMENTS - 2)); |
391 | memcpy(&art_user[i].data, &arts[i].data, sizeof(art_user[i].data)); |
392 | } |
393 | |
394 | if (copy_to_user(to: ubuf, from: art_user, n: art_len)) |
395 | ret = -EFAULT; |
396 | kfree(objp: art_user); |
397 | free_art: |
398 | kfree(objp: arts); |
399 | return ret; |
400 | } |
401 | |
402 | static int fill_trt(char __user *ubuf) |
403 | { |
404 | int i; |
405 | int ret; |
406 | int count; |
407 | int trt_len; |
408 | struct trt *trts = NULL; |
409 | union trt_object *trt_user; |
410 | |
411 | ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, &trts, false); |
412 | if (ret) |
413 | goto free_trt; |
414 | trt_len = count * sizeof(union trt_object); |
415 | trt_user = kzalloc(size: trt_len, GFP_KERNEL); |
416 | if (!trt_user) { |
417 | ret = -ENOMEM; |
418 | goto free_trt; |
419 | } |
420 | /* now fill in user trt data */ |
421 | for (i = 0; i < count; i++) { |
422 | /* userspace trt needs device name instead of acpi reference */ |
423 | get_single_name(handle: trts[i].source, name: trt_user[i].source_device); |
424 | get_single_name(handle: trts[i].target, name: trt_user[i].target_device); |
425 | trt_user[i].sample_period = trts[i].sample_period; |
426 | trt_user[i].influence = trts[i].influence; |
427 | } |
428 | |
429 | if (copy_to_user(to: ubuf, from: trt_user, n: trt_len)) |
430 | ret = -EFAULT; |
431 | kfree(objp: trt_user); |
432 | free_trt: |
433 | kfree(objp: trts); |
434 | return ret; |
435 | } |
436 | |
437 | static int fill_psvt(char __user *ubuf) |
438 | { |
439 | int i, ret, count, psvt_len; |
440 | union psvt_object *psvt_user; |
441 | struct psvt *psvts; |
442 | |
443 | ret = acpi_parse_psvt(handle: acpi_thermal_rel_handle, psvt_count: &count, psvtp: &psvts); |
444 | if (ret) |
445 | return ret; |
446 | |
447 | psvt_len = count * sizeof(*psvt_user); |
448 | |
449 | psvt_user = kzalloc(size: psvt_len, GFP_KERNEL); |
450 | if (!psvt_user) { |
451 | ret = -ENOMEM; |
452 | goto free_psvt; |
453 | } |
454 | |
455 | /* now fill in user psvt data */ |
456 | for (i = 0; i < count; i++) { |
457 | /* userspace psvt needs device name instead of acpi reference */ |
458 | get_single_name(handle: psvts[i].source, name: psvt_user[i].source_device); |
459 | get_single_name(handle: psvts[i].target, name: psvt_user[i].target_device); |
460 | |
461 | psvt_user[i].priority = psvts[i].priority; |
462 | psvt_user[i].sample_period = psvts[i].sample_period; |
463 | psvt_user[i].passive_temp = psvts[i].passive_temp; |
464 | psvt_user[i].source_domain = psvts[i].source_domain; |
465 | psvt_user[i].control_knob = psvts[i].control_knob; |
466 | psvt_user[i].step_size = psvts[i].step_size; |
467 | psvt_user[i].limit_coeff = psvts[i].limit_coeff; |
468 | psvt_user[i].unlimit_coeff = psvts[i].unlimit_coeff; |
469 | psvt_user[i].control_knob_type = psvts[i].control_knob_type; |
470 | if (psvt_user[i].control_knob_type == ACPI_TYPE_STRING) |
471 | strncpy(p: psvt_user[i].limit.string, q: psvts[i].limit.string, |
472 | ACPI_LIMIT_STR_MAX_LEN); |
473 | else |
474 | psvt_user[i].limit.integer = psvts[i].limit.integer; |
475 | |
476 | } |
477 | |
478 | if (copy_to_user(to: ubuf, from: psvt_user, n: psvt_len)) |
479 | ret = -EFAULT; |
480 | |
481 | kfree(objp: psvt_user); |
482 | |
483 | free_psvt: |
484 | kfree(objp: psvts); |
485 | return ret; |
486 | } |
487 | |
488 | static long acpi_thermal_rel_ioctl(struct file *f, unsigned int cmd, |
489 | unsigned long __arg) |
490 | { |
491 | int ret = 0; |
492 | unsigned long length = 0; |
493 | int count = 0; |
494 | char __user *arg = (void __user *)__arg; |
495 | struct trt *trts = NULL; |
496 | struct art *arts = NULL; |
497 | struct psvt *psvts; |
498 | |
499 | switch (cmd) { |
500 | case ACPI_THERMAL_GET_TRT_COUNT: |
501 | ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, |
502 | &trts, false); |
503 | kfree(objp: trts); |
504 | if (!ret) |
505 | return put_user(count, (unsigned long __user *)__arg); |
506 | return ret; |
507 | case ACPI_THERMAL_GET_TRT_LEN: |
508 | ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, |
509 | &trts, false); |
510 | kfree(objp: trts); |
511 | length = count * sizeof(union trt_object); |
512 | if (!ret) |
513 | return put_user(length, (unsigned long __user *)__arg); |
514 | return ret; |
515 | case ACPI_THERMAL_GET_TRT: |
516 | return fill_trt(ubuf: arg); |
517 | case ACPI_THERMAL_GET_ART_COUNT: |
518 | ret = acpi_parse_art(acpi_thermal_rel_handle, &count, |
519 | &arts, false); |
520 | kfree(objp: arts); |
521 | if (!ret) |
522 | return put_user(count, (unsigned long __user *)__arg); |
523 | return ret; |
524 | case ACPI_THERMAL_GET_ART_LEN: |
525 | ret = acpi_parse_art(acpi_thermal_rel_handle, &count, |
526 | &arts, false); |
527 | kfree(objp: arts); |
528 | length = count * sizeof(union art_object); |
529 | if (!ret) |
530 | return put_user(length, (unsigned long __user *)__arg); |
531 | return ret; |
532 | |
533 | case ACPI_THERMAL_GET_ART: |
534 | return fill_art(ubuf: arg); |
535 | |
536 | case ACPI_THERMAL_GET_PSVT_COUNT: |
537 | ret = acpi_parse_psvt(handle: acpi_thermal_rel_handle, psvt_count: &count, psvtp: &psvts); |
538 | if (!ret) { |
539 | kfree(objp: psvts); |
540 | return put_user(count, (unsigned long __user *)__arg); |
541 | } |
542 | return ret; |
543 | |
544 | case ACPI_THERMAL_GET_PSVT_LEN: |
545 | /* total length of the data retrieved (count * PSVT entry size) */ |
546 | ret = acpi_parse_psvt(handle: acpi_thermal_rel_handle, psvt_count: &count, psvtp: &psvts); |
547 | length = count * sizeof(union psvt_object); |
548 | if (!ret) { |
549 | kfree(objp: psvts); |
550 | return put_user(length, (unsigned long __user *)__arg); |
551 | } |
552 | return ret; |
553 | |
554 | case ACPI_THERMAL_GET_PSVT: |
555 | return fill_psvt(ubuf: arg); |
556 | |
557 | default: |
558 | return -ENOTTY; |
559 | } |
560 | } |
561 | |
562 | static const struct file_operations acpi_thermal_rel_fops = { |
563 | .owner = THIS_MODULE, |
564 | .open = acpi_thermal_rel_open, |
565 | .release = acpi_thermal_rel_release, |
566 | .unlocked_ioctl = acpi_thermal_rel_ioctl, |
567 | .llseek = no_llseek, |
568 | }; |
569 | |
570 | static struct miscdevice acpi_thermal_rel_misc_device = { |
571 | .minor = MISC_DYNAMIC_MINOR, |
572 | "acpi_thermal_rel" , |
573 | &acpi_thermal_rel_fops |
574 | }; |
575 | |
576 | int acpi_thermal_rel_misc_device_add(acpi_handle handle) |
577 | { |
578 | acpi_thermal_rel_handle = handle; |
579 | |
580 | return misc_register(misc: &acpi_thermal_rel_misc_device); |
581 | } |
582 | EXPORT_SYMBOL(acpi_thermal_rel_misc_device_add); |
583 | |
584 | int acpi_thermal_rel_misc_device_remove(acpi_handle handle) |
585 | { |
586 | misc_deregister(misc: &acpi_thermal_rel_misc_device); |
587 | |
588 | return 0; |
589 | } |
590 | EXPORT_SYMBOL(acpi_thermal_rel_misc_device_remove); |
591 | |
592 | MODULE_AUTHOR("Zhang Rui <rui.zhang@intel.com>" ); |
593 | MODULE_AUTHOR("Jacob Pan <jacob.jun.pan@intel.com" ); |
594 | MODULE_DESCRIPTION("Intel acpi thermal rel misc dev driver" ); |
595 | MODULE_LICENSE("GPL v2" ); |
596 | |