Merge branch 'topic/oxygen' into for-linus
[linux-2.6] / drivers / hwmon / hdaps.c
1 /*
2  * drivers/hwmon/hdaps.c - driver for IBM's Hard Drive Active Protection System
3  *
4  * Copyright (C) 2005 Robert Love <rml@novell.com>
5  * Copyright (C) 2005 Jesper Juhl <jesper.juhl@gmail.com>
6  *
7  * The HardDisk Active Protection System (hdaps) is present in IBM ThinkPads
8  * starting with the R40, T41, and X40.  It provides a basic two-axis
9  * accelerometer and other data, such as the device's temperature.
10  *
11  * This driver is based on the document by Mark A. Smith available at
12  * http://www.almaden.ibm.com/cs/people/marksmith/tpaps.html and a lot of trial
13  * and error.
14  *
15  * This program is free software; you can redistribute it and/or modify it
16  * under the terms of the GNU General Public License v2 as published by the
17  * Free Software Foundation.
18  *
19  * This program is distributed in the hope that it will be useful, but WITHOUT
20  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
21  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
22  * more details.
23  *
24  * You should have received a copy of the GNU General Public License along with
25  * this program; if not, write to the Free Software Foundation, Inc.,
26  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
27  */
28
29 #include <linux/delay.h>
30 #include <linux/platform_device.h>
31 #include <linux/input-polldev.h>
32 #include <linux/kernel.h>
33 #include <linux/mutex.h>
34 #include <linux/module.h>
35 #include <linux/timer.h>
36 #include <linux/dmi.h>
37 #include <linux/jiffies.h>
38
39 #include <asm/io.h>
40
41 #define HDAPS_LOW_PORT          0x1600  /* first port used by hdaps */
42 #define HDAPS_NR_PORTS          0x30    /* number of ports: 0x1600 - 0x162f */
43
44 #define HDAPS_PORT_STATE        0x1611  /* device state */
45 #define HDAPS_PORT_YPOS         0x1612  /* y-axis position */
46 #define HDAPS_PORT_XPOS         0x1614  /* x-axis position */
47 #define HDAPS_PORT_TEMP1        0x1616  /* device temperature, in Celsius */
48 #define HDAPS_PORT_YVAR         0x1617  /* y-axis variance (what is this?) */
49 #define HDAPS_PORT_XVAR         0x1619  /* x-axis variance (what is this?) */
50 #define HDAPS_PORT_TEMP2        0x161b  /* device temperature (again?) */
51 #define HDAPS_PORT_UNKNOWN      0x161c  /* what is this? */
52 #define HDAPS_PORT_KMACT        0x161d  /* keyboard or mouse activity */
53
54 #define STATE_FRESH             0x50    /* accelerometer data is fresh */
55
56 #define KEYBD_MASK              0x20    /* set if keyboard activity */
57 #define MOUSE_MASK              0x40    /* set if mouse activity */
58 #define KEYBD_ISSET(n)          (!! (n & KEYBD_MASK))   /* keyboard used? */
59 #define MOUSE_ISSET(n)          (!! (n & MOUSE_MASK))   /* mouse used? */
60
61 #define INIT_TIMEOUT_MSECS      4000    /* wait up to 4s for device init ... */
62 #define INIT_WAIT_MSECS         200     /* ... in 200ms increments */
63
64 #define HDAPS_POLL_INTERVAL     50      /* poll for input every 1/20s (50 ms)*/
65 #define HDAPS_INPUT_FUZZ        4       /* input event threshold */
66 #define HDAPS_INPUT_FLAT        4
67
68 #define HDAPS_X_AXIS            (1 << 0)
69 #define HDAPS_Y_AXIS            (1 << 1)
70 #define HDAPS_BOTH_AXES         (HDAPS_X_AXIS | HDAPS_Y_AXIS)
71
72 static struct platform_device *pdev;
73 static struct input_polled_dev *hdaps_idev;
74 static unsigned int hdaps_invert;
75 static u8 km_activity;
76 static int rest_x;
77 static int rest_y;
78
79 static DEFINE_MUTEX(hdaps_mtx);
80
81 /*
82  * __get_latch - Get the value from a given port.  Callers must hold hdaps_mtx.
83  */
84 static inline u8 __get_latch(u16 port)
85 {
86         return inb(port) & 0xff;
87 }
88
89 /*
90  * __check_latch - Check a port latch for a given value.  Returns zero if the
91  * port contains the given value.  Callers must hold hdaps_mtx.
92  */
93 static inline int __check_latch(u16 port, u8 val)
94 {
95         if (__get_latch(port) == val)
96                 return 0;
97         return -EINVAL;
98 }
99
100 /*
101  * __wait_latch - Wait up to 100us for a port latch to get a certain value,
102  * returning zero if the value is obtained.  Callers must hold hdaps_mtx.
103  */
104 static int __wait_latch(u16 port, u8 val)
105 {
106         unsigned int i;
107
108         for (i = 0; i < 20; i++) {
109                 if (!__check_latch(port, val))
110                         return 0;
111                 udelay(5);
112         }
113
114         return -EIO;
115 }
116
117 /*
118  * __device_refresh - request a refresh from the accelerometer.  Does not wait
119  * for refresh to complete.  Callers must hold hdaps_mtx.
120  */
121 static void __device_refresh(void)
122 {
123         udelay(200);
124         if (inb(0x1604) != STATE_FRESH) {
125                 outb(0x11, 0x1610);
126                 outb(0x01, 0x161f);
127         }
128 }
129
130 /*
131  * __device_refresh_sync - request a synchronous refresh from the
132  * accelerometer.  We wait for the refresh to complete.  Returns zero if
133  * successful and nonzero on error.  Callers must hold hdaps_mtx.
134  */
135 static int __device_refresh_sync(void)
136 {
137         __device_refresh();
138         return __wait_latch(0x1604, STATE_FRESH);
139 }
140
141 /*
142  * __device_complete - indicate to the accelerometer that we are done reading
143  * data, and then initiate an async refresh.  Callers must hold hdaps_mtx.
144  */
145 static inline void __device_complete(void)
146 {
147         inb(0x161f);
148         inb(0x1604);
149         __device_refresh();
150 }
151
152 /*
153  * hdaps_readb_one - reads a byte from a single I/O port, placing the value in
154  * the given pointer.  Returns zero on success or a negative error on failure.
155  * Can sleep.
156  */
157 static int hdaps_readb_one(unsigned int port, u8 *val)
158 {
159         int ret;
160
161         mutex_lock(&hdaps_mtx);
162
163         /* do a sync refresh -- we need to be sure that we read fresh data */
164         ret = __device_refresh_sync();
165         if (ret)
166                 goto out;
167
168         *val = inb(port);
169         __device_complete();
170
171 out:
172         mutex_unlock(&hdaps_mtx);
173         return ret;
174 }
175
176 /* __hdaps_read_pair - internal lockless helper for hdaps_read_pair(). */
177 static int __hdaps_read_pair(unsigned int port1, unsigned int port2,
178                              int *x, int *y)
179 {
180         /* do a sync refresh -- we need to be sure that we read fresh data */
181         if (__device_refresh_sync())
182                 return -EIO;
183
184         *y = inw(port2);
185         *x = inw(port1);
186         km_activity = inb(HDAPS_PORT_KMACT);
187         __device_complete();
188
189         /* hdaps_invert is a bitvector to negate the axes */
190         if (hdaps_invert & HDAPS_X_AXIS)
191                 *x = -*x;
192         if (hdaps_invert & HDAPS_Y_AXIS)
193                 *y = -*y;
194
195         return 0;
196 }
197
198 /*
199  * hdaps_read_pair - reads the values from a pair of ports, placing the values
200  * in the given pointers.  Returns zero on success.  Can sleep.
201  */
202 static int hdaps_read_pair(unsigned int port1, unsigned int port2,
203                            int *val1, int *val2)
204 {
205         int ret;
206
207         mutex_lock(&hdaps_mtx);
208         ret = __hdaps_read_pair(port1, port2, val1, val2);
209         mutex_unlock(&hdaps_mtx);
210
211         return ret;
212 }
213
214 /*
215  * hdaps_device_init - initialize the accelerometer.  Returns zero on success
216  * and negative error code on failure.  Can sleep.
217  */
218 static int hdaps_device_init(void)
219 {
220         int total, ret = -ENXIO;
221
222         mutex_lock(&hdaps_mtx);
223
224         outb(0x13, 0x1610);
225         outb(0x01, 0x161f);
226         if (__wait_latch(0x161f, 0x00))
227                 goto out;
228
229         /*
230          * Most ThinkPads return 0x01.
231          *
232          * Others--namely the R50p, T41p, and T42p--return 0x03.  These laptops
233          * have "inverted" axises.
234          *
235          * The 0x02 value occurs when the chip has been previously initialized.
236          */
237         if (__check_latch(0x1611, 0x03) &&
238                      __check_latch(0x1611, 0x02) &&
239                      __check_latch(0x1611, 0x01))
240                 goto out;
241
242         printk(KERN_DEBUG "hdaps: initial latch check good (0x%02x).\n",
243                __get_latch(0x1611));
244
245         outb(0x17, 0x1610);
246         outb(0x81, 0x1611);
247         outb(0x01, 0x161f);
248         if (__wait_latch(0x161f, 0x00))
249                 goto out;
250         if (__wait_latch(0x1611, 0x00))
251                 goto out;
252         if (__wait_latch(0x1612, 0x60))
253                 goto out;
254         if (__wait_latch(0x1613, 0x00))
255                 goto out;
256         outb(0x14, 0x1610);
257         outb(0x01, 0x1611);
258         outb(0x01, 0x161f);
259         if (__wait_latch(0x161f, 0x00))
260                 goto out;
261         outb(0x10, 0x1610);
262         outb(0xc8, 0x1611);
263         outb(0x00, 0x1612);
264         outb(0x02, 0x1613);
265         outb(0x01, 0x161f);
266         if (__wait_latch(0x161f, 0x00))
267                 goto out;
268         if (__device_refresh_sync())
269                 goto out;
270         if (__wait_latch(0x1611, 0x00))
271                 goto out;
272
273         /* we have done our dance, now let's wait for the applause */
274         for (total = INIT_TIMEOUT_MSECS; total > 0; total -= INIT_WAIT_MSECS) {
275                 int x, y;
276
277                 /* a read of the device helps push it into action */
278                 __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y);
279                 if (!__wait_latch(0x1611, 0x02)) {
280                         ret = 0;
281                         break;
282                 }
283
284                 msleep(INIT_WAIT_MSECS);
285         }
286
287 out:
288         mutex_unlock(&hdaps_mtx);
289         return ret;
290 }
291
292
293 /* Device model stuff */
294
295 static int hdaps_probe(struct platform_device *dev)
296 {
297         int ret;
298
299         ret = hdaps_device_init();
300         if (ret)
301                 return ret;
302
303         printk(KERN_INFO "hdaps: device successfully initialized.\n");
304         return 0;
305 }
306
307 static int hdaps_resume(struct platform_device *dev)
308 {
309         return hdaps_device_init();
310 }
311
312 static struct platform_driver hdaps_driver = {
313         .probe = hdaps_probe,
314         .resume = hdaps_resume,
315         .driver = {
316                 .name = "hdaps",
317                 .owner = THIS_MODULE,
318         },
319 };
320
321 /*
322  * hdaps_calibrate - Set our "resting" values.  Callers must hold hdaps_mtx.
323  */
324 static void hdaps_calibrate(void)
325 {
326         __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &rest_x, &rest_y);
327 }
328
329 static void hdaps_mousedev_poll(struct input_polled_dev *dev)
330 {
331         struct input_dev *input_dev = dev->input;
332         int x, y;
333
334         mutex_lock(&hdaps_mtx);
335
336         if (__hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y))
337                 goto out;
338
339         input_report_abs(input_dev, ABS_X, x - rest_x);
340         input_report_abs(input_dev, ABS_Y, y - rest_y);
341         input_sync(input_dev);
342
343 out:
344         mutex_unlock(&hdaps_mtx);
345 }
346
347
348 /* Sysfs Files */
349
350 static ssize_t hdaps_position_show(struct device *dev,
351                                    struct device_attribute *attr, char *buf)
352 {
353         int ret, x, y;
354
355         ret = hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y);
356         if (ret)
357                 return ret;
358
359         return sprintf(buf, "(%d,%d)\n", x, y);
360 }
361
362 static ssize_t hdaps_variance_show(struct device *dev,
363                                    struct device_attribute *attr, char *buf)
364 {
365         int ret, x, y;
366
367         ret = hdaps_read_pair(HDAPS_PORT_XVAR, HDAPS_PORT_YVAR, &x, &y);
368         if (ret)
369                 return ret;
370
371         return sprintf(buf, "(%d,%d)\n", x, y);
372 }
373
374 static ssize_t hdaps_temp1_show(struct device *dev,
375                                 struct device_attribute *attr, char *buf)
376 {
377         u8 temp;
378         int ret;
379
380         ret = hdaps_readb_one(HDAPS_PORT_TEMP1, &temp);
381         if (ret < 0)
382                 return ret;
383
384         return sprintf(buf, "%u\n", temp);
385 }
386
387 static ssize_t hdaps_temp2_show(struct device *dev,
388                                 struct device_attribute *attr, char *buf)
389 {
390         u8 temp;
391         int ret;
392
393         ret = hdaps_readb_one(HDAPS_PORT_TEMP2, &temp);
394         if (ret < 0)
395                 return ret;
396
397         return sprintf(buf, "%u\n", temp);
398 }
399
400 static ssize_t hdaps_keyboard_activity_show(struct device *dev,
401                                             struct device_attribute *attr,
402                                             char *buf)
403 {
404         return sprintf(buf, "%u\n", KEYBD_ISSET(km_activity));
405 }
406
407 static ssize_t hdaps_mouse_activity_show(struct device *dev,
408                                          struct device_attribute *attr,
409                                          char *buf)
410 {
411         return sprintf(buf, "%u\n", MOUSE_ISSET(km_activity));
412 }
413
414 static ssize_t hdaps_calibrate_show(struct device *dev,
415                                     struct device_attribute *attr, char *buf)
416 {
417         return sprintf(buf, "(%d,%d)\n", rest_x, rest_y);
418 }
419
420 static ssize_t hdaps_calibrate_store(struct device *dev,
421                                      struct device_attribute *attr,
422                                      const char *buf, size_t count)
423 {
424         mutex_lock(&hdaps_mtx);
425         hdaps_calibrate();
426         mutex_unlock(&hdaps_mtx);
427
428         return count;
429 }
430
431 static ssize_t hdaps_invert_show(struct device *dev,
432                                  struct device_attribute *attr, char *buf)
433 {
434         return sprintf(buf, "%u\n", hdaps_invert);
435 }
436
437 static ssize_t hdaps_invert_store(struct device *dev,
438                                   struct device_attribute *attr,
439                                   const char *buf, size_t count)
440 {
441         int invert;
442
443         if (sscanf(buf, "%d", &invert) != 1 ||
444             invert < 0 || invert > HDAPS_BOTH_AXES)
445                 return -EINVAL;
446
447         hdaps_invert = invert;
448         hdaps_calibrate();
449
450         return count;
451 }
452
453 static DEVICE_ATTR(position, 0444, hdaps_position_show, NULL);
454 static DEVICE_ATTR(variance, 0444, hdaps_variance_show, NULL);
455 static DEVICE_ATTR(temp1, 0444, hdaps_temp1_show, NULL);
456 static DEVICE_ATTR(temp2, 0444, hdaps_temp2_show, NULL);
457 static DEVICE_ATTR(keyboard_activity, 0444, hdaps_keyboard_activity_show, NULL);
458 static DEVICE_ATTR(mouse_activity, 0444, hdaps_mouse_activity_show, NULL);
459 static DEVICE_ATTR(calibrate, 0644, hdaps_calibrate_show,hdaps_calibrate_store);
460 static DEVICE_ATTR(invert, 0644, hdaps_invert_show, hdaps_invert_store);
461
462 static struct attribute *hdaps_attributes[] = {
463         &dev_attr_position.attr,
464         &dev_attr_variance.attr,
465         &dev_attr_temp1.attr,
466         &dev_attr_temp2.attr,
467         &dev_attr_keyboard_activity.attr,
468         &dev_attr_mouse_activity.attr,
469         &dev_attr_calibrate.attr,
470         &dev_attr_invert.attr,
471         NULL,
472 };
473
474 static struct attribute_group hdaps_attribute_group = {
475         .attrs = hdaps_attributes,
476 };
477
478
479 /* Module stuff */
480
481 /* hdaps_dmi_match - found a match.  return one, short-circuiting the hunt. */
482 static int __init hdaps_dmi_match(const struct dmi_system_id *id)
483 {
484         printk(KERN_INFO "hdaps: %s detected.\n", id->ident);
485         return 1;
486 }
487
488 /* hdaps_dmi_match_invert - found an inverted match. */
489 static int __init hdaps_dmi_match_invert(const struct dmi_system_id *id)
490 {
491         hdaps_invert = (unsigned long)id->driver_data;
492         printk(KERN_INFO "hdaps: inverting axis (%u) readings.\n",
493                hdaps_invert);
494         return hdaps_dmi_match(id);
495 }
496
497 #define HDAPS_DMI_MATCH_INVERT(vendor, model, axes) {   \
498         .ident = vendor " " model,                      \
499         .callback = hdaps_dmi_match_invert,             \
500         .driver_data = (void *)axes,                    \
501         .matches = {                                    \
502                 DMI_MATCH(DMI_BOARD_VENDOR, vendor),    \
503                 DMI_MATCH(DMI_PRODUCT_VERSION, model)   \
504         }                                               \
505 }
506
507 #define HDAPS_DMI_MATCH_NORMAL(vendor, model)           \
508         HDAPS_DMI_MATCH_INVERT(vendor, model, 0)
509
510 /* Note that HDAPS_DMI_MATCH_NORMAL("ThinkPad T42") would match
511    "ThinkPad T42p", so the order of the entries matters.
512    If your ThinkPad is not recognized, please update to latest
513    BIOS. This is especially the case for some R52 ThinkPads. */
514 static struct dmi_system_id __initdata hdaps_whitelist[] = {
515         HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad R50p", HDAPS_BOTH_AXES),
516         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R50"),
517         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R51"),
518         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad R52"),
519         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad R61i", HDAPS_BOTH_AXES),
520         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad R61", HDAPS_BOTH_AXES),
521         HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad T41p", HDAPS_BOTH_AXES),
522         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T41"),
523         HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad T42p", HDAPS_BOTH_AXES),
524         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T42"),
525         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad T43"),
526         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T60", HDAPS_BOTH_AXES),
527         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T61p", HDAPS_BOTH_AXES),
528         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad T61", HDAPS_BOTH_AXES),
529         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad X40"),
530         HDAPS_DMI_MATCH_INVERT("IBM", "ThinkPad X41", HDAPS_Y_AXIS),
531         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X60", HDAPS_BOTH_AXES),
532         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X61s", HDAPS_BOTH_AXES),
533         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad X61", HDAPS_BOTH_AXES),
534         HDAPS_DMI_MATCH_NORMAL("IBM", "ThinkPad Z60m"),
535         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad Z61m", HDAPS_BOTH_AXES),
536         HDAPS_DMI_MATCH_INVERT("LENOVO", "ThinkPad Z61p", HDAPS_BOTH_AXES),
537         { .ident = NULL }
538 };
539
540 static int __init hdaps_init(void)
541 {
542         struct input_dev *idev;
543         int ret;
544
545         if (!dmi_check_system(hdaps_whitelist)) {
546                 printk(KERN_WARNING "hdaps: supported laptop not found!\n");
547                 ret = -ENODEV;
548                 goto out;
549         }
550
551         if (!request_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS, "hdaps")) {
552                 ret = -ENXIO;
553                 goto out;
554         }
555
556         ret = platform_driver_register(&hdaps_driver);
557         if (ret)
558                 goto out_region;
559
560         pdev = platform_device_register_simple("hdaps", -1, NULL, 0);
561         if (IS_ERR(pdev)) {
562                 ret = PTR_ERR(pdev);
563                 goto out_driver;
564         }
565
566         ret = sysfs_create_group(&pdev->dev.kobj, &hdaps_attribute_group);
567         if (ret)
568                 goto out_device;
569
570         hdaps_idev = input_allocate_polled_device();
571         if (!hdaps_idev) {
572                 ret = -ENOMEM;
573                 goto out_group;
574         }
575
576         hdaps_idev->poll = hdaps_mousedev_poll;
577         hdaps_idev->poll_interval = HDAPS_POLL_INTERVAL;
578
579         /* initial calibrate for the input device */
580         hdaps_calibrate();
581
582         /* initialize the input class */
583         idev = hdaps_idev->input;
584         idev->name = "hdaps";
585         idev->phys = "isa1600/input0";
586         idev->id.bustype = BUS_ISA;
587         idev->dev.parent = &pdev->dev;
588         idev->evbit[0] = BIT_MASK(EV_ABS);
589         input_set_abs_params(idev, ABS_X,
590                         -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT);
591         input_set_abs_params(idev, ABS_Y,
592                         -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT);
593
594         ret = input_register_polled_device(hdaps_idev);
595         if (ret)
596                 goto out_idev;
597
598         printk(KERN_INFO "hdaps: driver successfully loaded.\n");
599         return 0;
600
601 out_idev:
602         input_free_polled_device(hdaps_idev);
603 out_group:
604         sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group);
605 out_device:
606         platform_device_unregister(pdev);
607 out_driver:
608         platform_driver_unregister(&hdaps_driver);
609 out_region:
610         release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
611 out:
612         printk(KERN_WARNING "hdaps: driver init failed (ret=%d)!\n", ret);
613         return ret;
614 }
615
616 static void __exit hdaps_exit(void)
617 {
618         input_unregister_polled_device(hdaps_idev);
619         input_free_polled_device(hdaps_idev);
620         sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group);
621         platform_device_unregister(pdev);
622         platform_driver_unregister(&hdaps_driver);
623         release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
624
625         printk(KERN_INFO "hdaps: driver unloaded.\n");
626 }
627
628 module_init(hdaps_init);
629 module_exit(hdaps_exit);
630
631 module_param_named(invert, hdaps_invert, int, 0);
632 MODULE_PARM_DESC(invert, "invert data along each axis. 1 invert x-axis, "
633                  "2 invert y-axis, 3 invert both axes.");
634
635 MODULE_AUTHOR("Robert Love");
636 MODULE_DESCRIPTION("IBM Hard Drive Active Protection System (HDAPS) driver");
637 MODULE_LICENSE("GPL v2");