hdaps.c 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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. #include <linux/delay.h>
  29. #include <linux/platform_device.h>
  30. #include <linux/input.h>
  31. #include <linux/kernel.h>
  32. #include <linux/module.h>
  33. #include <linux/timer.h>
  34. #include <linux/dmi.h>
  35. #include <asm/io.h>
  36. #define HDAPS_LOW_PORT 0x1600 /* first port used by hdaps */
  37. #define HDAPS_NR_PORTS 0x30 /* number of ports: 0x1600 - 0x162f */
  38. #define HDAPS_PORT_STATE 0x1611 /* device state */
  39. #define HDAPS_PORT_YPOS 0x1612 /* y-axis position */
  40. #define HDAPS_PORT_XPOS 0x1614 /* x-axis position */
  41. #define HDAPS_PORT_TEMP1 0x1616 /* device temperature, in celcius */
  42. #define HDAPS_PORT_YVAR 0x1617 /* y-axis variance (what is this?) */
  43. #define HDAPS_PORT_XVAR 0x1619 /* x-axis variance (what is this?) */
  44. #define HDAPS_PORT_TEMP2 0x161b /* device temperature (again?) */
  45. #define HDAPS_PORT_UNKNOWN 0x161c /* what is this? */
  46. #define HDAPS_PORT_KMACT 0x161d /* keyboard or mouse activity */
  47. #define STATE_FRESH 0x50 /* accelerometer data is fresh */
  48. #define KEYBD_MASK 0x20 /* set if keyboard activity */
  49. #define MOUSE_MASK 0x40 /* set if mouse activity */
  50. #define KEYBD_ISSET(n) (!! (n & KEYBD_MASK)) /* keyboard used? */
  51. #define MOUSE_ISSET(n) (!! (n & MOUSE_MASK)) /* mouse used? */
  52. #define INIT_TIMEOUT_MSECS 4000 /* wait up to 4s for device init ... */
  53. #define INIT_WAIT_MSECS 200 /* ... in 200ms increments */
  54. #define HDAPS_POLL_PERIOD (HZ/20) /* poll for input every 1/20s */
  55. #define HDAPS_INPUT_FUZZ 4 /* input event threshold */
  56. #define HDAPS_INPUT_FLAT 4
  57. static struct timer_list hdaps_timer;
  58. static struct platform_device *pdev;
  59. static struct input_dev *hdaps_idev;
  60. static unsigned int hdaps_invert;
  61. static u8 km_activity;
  62. static int rest_x;
  63. static int rest_y;
  64. static DECLARE_MUTEX(hdaps_sem);
  65. /*
  66. * __get_latch - Get the value from a given port. Callers must hold hdaps_sem.
  67. */
  68. static inline u8 __get_latch(u16 port)
  69. {
  70. return inb(port) & 0xff;
  71. }
  72. /*
  73. * __check_latch - Check a port latch for a given value. Returns zero if the
  74. * port contains the given value. Callers must hold hdaps_sem.
  75. */
  76. static inline int __check_latch(u16 port, u8 val)
  77. {
  78. if (__get_latch(port) == val)
  79. return 0;
  80. return -EINVAL;
  81. }
  82. /*
  83. * __wait_latch - Wait up to 100us for a port latch to get a certain value,
  84. * returning zero if the value is obtained. Callers must hold hdaps_sem.
  85. */
  86. static int __wait_latch(u16 port, u8 val)
  87. {
  88. unsigned int i;
  89. for (i = 0; i < 20; i++) {
  90. if (!__check_latch(port, val))
  91. return 0;
  92. udelay(5);
  93. }
  94. return -EIO;
  95. }
  96. /*
  97. * __device_refresh - request a refresh from the accelerometer. Does not wait
  98. * for refresh to complete. Callers must hold hdaps_sem.
  99. */
  100. static void __device_refresh(void)
  101. {
  102. udelay(200);
  103. if (inb(0x1604) != STATE_FRESH) {
  104. outb(0x11, 0x1610);
  105. outb(0x01, 0x161f);
  106. }
  107. }
  108. /*
  109. * __device_refresh_sync - request a synchronous refresh from the
  110. * accelerometer. We wait for the refresh to complete. Returns zero if
  111. * successful and nonzero on error. Callers must hold hdaps_sem.
  112. */
  113. static int __device_refresh_sync(void)
  114. {
  115. __device_refresh();
  116. return __wait_latch(0x1604, STATE_FRESH);
  117. }
  118. /*
  119. * __device_complete - indicate to the accelerometer that we are done reading
  120. * data, and then initiate an async refresh. Callers must hold hdaps_sem.
  121. */
  122. static inline void __device_complete(void)
  123. {
  124. inb(0x161f);
  125. inb(0x1604);
  126. __device_refresh();
  127. }
  128. /*
  129. * hdaps_readb_one - reads a byte from a single I/O port, placing the value in
  130. * the given pointer. Returns zero on success or a negative error on failure.
  131. * Can sleep.
  132. */
  133. static int hdaps_readb_one(unsigned int port, u8 *val)
  134. {
  135. int ret;
  136. down(&hdaps_sem);
  137. /* do a sync refresh -- we need to be sure that we read fresh data */
  138. ret = __device_refresh_sync();
  139. if (ret)
  140. goto out;
  141. *val = inb(port);
  142. __device_complete();
  143. out:
  144. up(&hdaps_sem);
  145. return ret;
  146. }
  147. /* __hdaps_read_pair - internal lockless helper for hdaps_read_pair(). */
  148. static int __hdaps_read_pair(unsigned int port1, unsigned int port2,
  149. int *x, int *y)
  150. {
  151. /* do a sync refresh -- we need to be sure that we read fresh data */
  152. if (__device_refresh_sync())
  153. return -EIO;
  154. *y = inw(port2);
  155. *x = inw(port1);
  156. km_activity = inb(HDAPS_PORT_KMACT);
  157. __device_complete();
  158. /* if hdaps_invert is set, negate the two values */
  159. if (hdaps_invert) {
  160. *x = -*x;
  161. *y = -*y;
  162. }
  163. return 0;
  164. }
  165. /*
  166. * hdaps_read_pair - reads the values from a pair of ports, placing the values
  167. * in the given pointers. Returns zero on success. Can sleep.
  168. */
  169. static int hdaps_read_pair(unsigned int port1, unsigned int port2,
  170. int *val1, int *val2)
  171. {
  172. int ret;
  173. down(&hdaps_sem);
  174. ret = __hdaps_read_pair(port1, port2, val1, val2);
  175. up(&hdaps_sem);
  176. return ret;
  177. }
  178. /*
  179. * hdaps_device_init - initialize the accelerometer. Returns zero on success
  180. * and negative error code on failure. Can sleep.
  181. */
  182. static int hdaps_device_init(void)
  183. {
  184. int total, ret = -ENXIO;
  185. down(&hdaps_sem);
  186. outb(0x13, 0x1610);
  187. outb(0x01, 0x161f);
  188. if (__wait_latch(0x161f, 0x00))
  189. goto out;
  190. /*
  191. * Most ThinkPads return 0x01.
  192. *
  193. * Others--namely the R50p, T41p, and T42p--return 0x03. These laptops
  194. * have "inverted" axises.
  195. *
  196. * The 0x02 value occurs when the chip has been previously initialized.
  197. */
  198. if (__check_latch(0x1611, 0x03) &&
  199. __check_latch(0x1611, 0x02) &&
  200. __check_latch(0x1611, 0x01))
  201. goto out;
  202. printk(KERN_DEBUG "hdaps: initial latch check good (0x%02x).\n",
  203. __get_latch(0x1611));
  204. outb(0x17, 0x1610);
  205. outb(0x81, 0x1611);
  206. outb(0x01, 0x161f);
  207. if (__wait_latch(0x161f, 0x00))
  208. goto out;
  209. if (__wait_latch(0x1611, 0x00))
  210. goto out;
  211. if (__wait_latch(0x1612, 0x60))
  212. goto out;
  213. if (__wait_latch(0x1613, 0x00))
  214. goto out;
  215. outb(0x14, 0x1610);
  216. outb(0x01, 0x1611);
  217. outb(0x01, 0x161f);
  218. if (__wait_latch(0x161f, 0x00))
  219. goto out;
  220. outb(0x10, 0x1610);
  221. outb(0xc8, 0x1611);
  222. outb(0x00, 0x1612);
  223. outb(0x02, 0x1613);
  224. outb(0x01, 0x161f);
  225. if (__wait_latch(0x161f, 0x00))
  226. goto out;
  227. if (__device_refresh_sync())
  228. goto out;
  229. if (__wait_latch(0x1611, 0x00))
  230. goto out;
  231. /* we have done our dance, now let's wait for the applause */
  232. for (total = INIT_TIMEOUT_MSECS; total > 0; total -= INIT_WAIT_MSECS) {
  233. int x, y;
  234. /* a read of the device helps push it into action */
  235. __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y);
  236. if (!__wait_latch(0x1611, 0x02)) {
  237. ret = 0;
  238. break;
  239. }
  240. msleep(INIT_WAIT_MSECS);
  241. }
  242. out:
  243. up(&hdaps_sem);
  244. return ret;
  245. }
  246. /* Device model stuff */
  247. static int hdaps_probe(struct platform_device *dev)
  248. {
  249. int ret;
  250. ret = hdaps_device_init();
  251. if (ret)
  252. return ret;
  253. printk(KERN_INFO "hdaps: device successfully initialized.\n");
  254. return 0;
  255. }
  256. static int hdaps_resume(struct platform_device *dev)
  257. {
  258. return hdaps_device_init();
  259. }
  260. static struct platform_driver hdaps_driver = {
  261. .probe = hdaps_probe,
  262. .resume = hdaps_resume,
  263. .driver = {
  264. .name = "hdaps",
  265. .owner = THIS_MODULE,
  266. },
  267. };
  268. /*
  269. * hdaps_calibrate - Set our "resting" values. Callers must hold hdaps_sem.
  270. */
  271. static void hdaps_calibrate(void)
  272. {
  273. __hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &rest_x, &rest_y);
  274. }
  275. static void hdaps_mousedev_poll(unsigned long unused)
  276. {
  277. int x, y;
  278. /* Cannot sleep. Try nonblockingly. If we fail, try again later. */
  279. if (down_trylock(&hdaps_sem)) {
  280. mod_timer(&hdaps_timer,jiffies + HDAPS_POLL_PERIOD);
  281. return;
  282. }
  283. if (__hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y))
  284. goto out;
  285. input_report_abs(hdaps_idev, ABS_X, x - rest_x);
  286. input_report_abs(hdaps_idev, ABS_Y, y - rest_y);
  287. input_sync(hdaps_idev);
  288. mod_timer(&hdaps_timer, jiffies + HDAPS_POLL_PERIOD);
  289. out:
  290. up(&hdaps_sem);
  291. }
  292. /* Sysfs Files */
  293. static ssize_t hdaps_position_show(struct device *dev,
  294. struct device_attribute *attr, char *buf)
  295. {
  296. int ret, x, y;
  297. ret = hdaps_read_pair(HDAPS_PORT_XPOS, HDAPS_PORT_YPOS, &x, &y);
  298. if (ret)
  299. return ret;
  300. return sprintf(buf, "(%d,%d)\n", x, y);
  301. }
  302. static ssize_t hdaps_variance_show(struct device *dev,
  303. struct device_attribute *attr, char *buf)
  304. {
  305. int ret, x, y;
  306. ret = hdaps_read_pair(HDAPS_PORT_XVAR, HDAPS_PORT_YVAR, &x, &y);
  307. if (ret)
  308. return ret;
  309. return sprintf(buf, "(%d,%d)\n", x, y);
  310. }
  311. static ssize_t hdaps_temp1_show(struct device *dev,
  312. struct device_attribute *attr, char *buf)
  313. {
  314. u8 temp;
  315. int ret;
  316. ret = hdaps_readb_one(HDAPS_PORT_TEMP1, &temp);
  317. if (ret < 0)
  318. return ret;
  319. return sprintf(buf, "%u\n", temp);
  320. }
  321. static ssize_t hdaps_temp2_show(struct device *dev,
  322. struct device_attribute *attr, char *buf)
  323. {
  324. u8 temp;
  325. int ret;
  326. ret = hdaps_readb_one(HDAPS_PORT_TEMP2, &temp);
  327. if (ret < 0)
  328. return ret;
  329. return sprintf(buf, "%u\n", temp);
  330. }
  331. static ssize_t hdaps_keyboard_activity_show(struct device *dev,
  332. struct device_attribute *attr,
  333. char *buf)
  334. {
  335. return sprintf(buf, "%u\n", KEYBD_ISSET(km_activity));
  336. }
  337. static ssize_t hdaps_mouse_activity_show(struct device *dev,
  338. struct device_attribute *attr,
  339. char *buf)
  340. {
  341. return sprintf(buf, "%u\n", MOUSE_ISSET(km_activity));
  342. }
  343. static ssize_t hdaps_calibrate_show(struct device *dev,
  344. struct device_attribute *attr, char *buf)
  345. {
  346. return sprintf(buf, "(%d,%d)\n", rest_x, rest_y);
  347. }
  348. static ssize_t hdaps_calibrate_store(struct device *dev,
  349. struct device_attribute *attr,
  350. const char *buf, size_t count)
  351. {
  352. down(&hdaps_sem);
  353. hdaps_calibrate();
  354. up(&hdaps_sem);
  355. return count;
  356. }
  357. static ssize_t hdaps_invert_show(struct device *dev,
  358. struct device_attribute *attr, char *buf)
  359. {
  360. return sprintf(buf, "%u\n", hdaps_invert);
  361. }
  362. static ssize_t hdaps_invert_store(struct device *dev,
  363. struct device_attribute *attr,
  364. const char *buf, size_t count)
  365. {
  366. int invert;
  367. if (sscanf(buf, "%d", &invert) != 1 || (invert != 1 && invert != 0))
  368. return -EINVAL;
  369. hdaps_invert = invert;
  370. hdaps_calibrate();
  371. return count;
  372. }
  373. static DEVICE_ATTR(position, 0444, hdaps_position_show, NULL);
  374. static DEVICE_ATTR(variance, 0444, hdaps_variance_show, NULL);
  375. static DEVICE_ATTR(temp1, 0444, hdaps_temp1_show, NULL);
  376. static DEVICE_ATTR(temp2, 0444, hdaps_temp2_show, NULL);
  377. static DEVICE_ATTR(keyboard_activity, 0444, hdaps_keyboard_activity_show, NULL);
  378. static DEVICE_ATTR(mouse_activity, 0444, hdaps_mouse_activity_show, NULL);
  379. static DEVICE_ATTR(calibrate, 0644, hdaps_calibrate_show,hdaps_calibrate_store);
  380. static DEVICE_ATTR(invert, 0644, hdaps_invert_show, hdaps_invert_store);
  381. static struct attribute *hdaps_attributes[] = {
  382. &dev_attr_position.attr,
  383. &dev_attr_variance.attr,
  384. &dev_attr_temp1.attr,
  385. &dev_attr_temp2.attr,
  386. &dev_attr_keyboard_activity.attr,
  387. &dev_attr_mouse_activity.attr,
  388. &dev_attr_calibrate.attr,
  389. &dev_attr_invert.attr,
  390. NULL,
  391. };
  392. static struct attribute_group hdaps_attribute_group = {
  393. .attrs = hdaps_attributes,
  394. };
  395. /* Module stuff */
  396. /* hdaps_dmi_match - found a match. return one, short-circuiting the hunt. */
  397. static int hdaps_dmi_match(struct dmi_system_id *id)
  398. {
  399. printk(KERN_INFO "hdaps: %s detected.\n", id->ident);
  400. return 1;
  401. }
  402. /* hdaps_dmi_match_invert - found an inverted match. */
  403. static int hdaps_dmi_match_invert(struct dmi_system_id *id)
  404. {
  405. hdaps_invert = 1;
  406. printk(KERN_INFO "hdaps: inverting axis readings.\n");
  407. return hdaps_dmi_match(id);
  408. }
  409. #define HDAPS_DMI_MATCH_NORMAL(model) { \
  410. .ident = "IBM " model, \
  411. .callback = hdaps_dmi_match, \
  412. .matches = { \
  413. DMI_MATCH(DMI_BOARD_VENDOR, "IBM"), \
  414. DMI_MATCH(DMI_PRODUCT_VERSION, model) \
  415. } \
  416. }
  417. #define HDAPS_DMI_MATCH_INVERT(model) { \
  418. .ident = "IBM " model, \
  419. .callback = hdaps_dmi_match_invert, \
  420. .matches = { \
  421. DMI_MATCH(DMI_BOARD_VENDOR, "IBM"), \
  422. DMI_MATCH(DMI_PRODUCT_VERSION, model) \
  423. } \
  424. }
  425. #define HDAPS_DMI_MATCH_LENOVO(model) { \
  426. .ident = "Lenovo " model, \
  427. .callback = hdaps_dmi_match_invert, \
  428. .matches = { \
  429. DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"), \
  430. DMI_MATCH(DMI_PRODUCT_VERSION, model) \
  431. } \
  432. }
  433. static int __init hdaps_init(void)
  434. {
  435. int ret;
  436. /* Note that DMI_MATCH(...,"ThinkPad T42") will match "ThinkPad T42p" */
  437. struct dmi_system_id hdaps_whitelist[] = {
  438. HDAPS_DMI_MATCH_NORMAL("ThinkPad H"),
  439. HDAPS_DMI_MATCH_INVERT("ThinkPad R50p"),
  440. HDAPS_DMI_MATCH_NORMAL("ThinkPad R50"),
  441. HDAPS_DMI_MATCH_NORMAL("ThinkPad R51"),
  442. HDAPS_DMI_MATCH_NORMAL("ThinkPad R52"),
  443. HDAPS_DMI_MATCH_INVERT("ThinkPad T41p"),
  444. HDAPS_DMI_MATCH_NORMAL("ThinkPad T41"),
  445. HDAPS_DMI_MATCH_INVERT("ThinkPad T42p"),
  446. HDAPS_DMI_MATCH_NORMAL("ThinkPad T42"),
  447. HDAPS_DMI_MATCH_NORMAL("ThinkPad T43"),
  448. HDAPS_DMI_MATCH_LENOVO("ThinkPad T60p"),
  449. HDAPS_DMI_MATCH_NORMAL("ThinkPad X40"),
  450. HDAPS_DMI_MATCH_NORMAL("ThinkPad X41 Tablet"),
  451. HDAPS_DMI_MATCH_NORMAL("ThinkPad X41"),
  452. HDAPS_DMI_MATCH_LENOVO("ThinkPad X60"),
  453. { .ident = NULL }
  454. };
  455. if (!dmi_check_system(hdaps_whitelist)) {
  456. printk(KERN_WARNING "hdaps: supported laptop not found!\n");
  457. ret = -ENODEV;
  458. goto out;
  459. }
  460. if (!request_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS, "hdaps")) {
  461. ret = -ENXIO;
  462. goto out;
  463. }
  464. ret = platform_driver_register(&hdaps_driver);
  465. if (ret)
  466. goto out_region;
  467. pdev = platform_device_register_simple("hdaps", -1, NULL, 0);
  468. if (IS_ERR(pdev)) {
  469. ret = PTR_ERR(pdev);
  470. goto out_driver;
  471. }
  472. ret = sysfs_create_group(&pdev->dev.kobj, &hdaps_attribute_group);
  473. if (ret)
  474. goto out_device;
  475. hdaps_idev = input_allocate_device();
  476. if (!hdaps_idev) {
  477. ret = -ENOMEM;
  478. goto out_group;
  479. }
  480. /* initial calibrate for the input device */
  481. hdaps_calibrate();
  482. /* initialize the input class */
  483. hdaps_idev->name = "hdaps";
  484. hdaps_idev->cdev.dev = &pdev->dev;
  485. hdaps_idev->evbit[0] = BIT(EV_ABS);
  486. input_set_abs_params(hdaps_idev, ABS_X,
  487. -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT);
  488. input_set_abs_params(hdaps_idev, ABS_Y,
  489. -256, 256, HDAPS_INPUT_FUZZ, HDAPS_INPUT_FLAT);
  490. input_register_device(hdaps_idev);
  491. /* start up our timer for the input device */
  492. init_timer(&hdaps_timer);
  493. hdaps_timer.function = hdaps_mousedev_poll;
  494. hdaps_timer.expires = jiffies + HDAPS_POLL_PERIOD;
  495. add_timer(&hdaps_timer);
  496. printk(KERN_INFO "hdaps: driver successfully loaded.\n");
  497. return 0;
  498. out_group:
  499. sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group);
  500. out_device:
  501. platform_device_unregister(pdev);
  502. out_driver:
  503. platform_driver_unregister(&hdaps_driver);
  504. out_region:
  505. release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
  506. out:
  507. printk(KERN_WARNING "hdaps: driver init failed (ret=%d)!\n", ret);
  508. return ret;
  509. }
  510. static void __exit hdaps_exit(void)
  511. {
  512. del_timer_sync(&hdaps_timer);
  513. input_unregister_device(hdaps_idev);
  514. sysfs_remove_group(&pdev->dev.kobj, &hdaps_attribute_group);
  515. platform_device_unregister(pdev);
  516. platform_driver_unregister(&hdaps_driver);
  517. release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
  518. printk(KERN_INFO "hdaps: driver unloaded.\n");
  519. }
  520. module_init(hdaps_init);
  521. module_exit(hdaps_exit);
  522. module_param_named(invert, hdaps_invert, bool, 0);
  523. MODULE_PARM_DESC(invert, "invert data along each axis");
  524. MODULE_AUTHOR("Robert Love");
  525. MODULE_DESCRIPTION("IBM Hard Drive Active Protection System (HDAPS) driver");
  526. MODULE_LICENSE("GPL v2");