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