summaryrefslogtreecommitdiff
path: root/drivers/platform/x86/hp_accel.c
blob: 54a4addc7903dc28ab826158577560d2595b18f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  hp_accel.c - Interface between LIS3LV02DL driver and HP ACPI BIOS
 *
 *  Copyright (C) 2007-2008 Yan Burman
 *  Copyright (C) 2008 Eric Piel
 *  Copyright (C) 2008-2009 Pavel Machek
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/dmi.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/freezer.h>
#include <linux/uaccess.h>
#include <linux/leds.h>
#include <linux/atomic.h>
#include <linux/acpi.h>
#include <linux/i8042.h>
#include <linux/serio.h>
#include "../../misc/lis3lv02d/lis3lv02d.h"

#define DRIVER_NAME     "hp_accel"
#define ACPI_MDPS_CLASS "accelerometer"

/* Delayed LEDs infrastructure ------------------------------------ */

/* Special LED class that can defer work */
struct delayed_led_classdev {
	struct led_classdev led_classdev;
	struct work_struct work;
	enum led_brightness new_brightness;

	unsigned int led;		/* For driver */
	void (*set_brightness)(struct delayed_led_classdev *data, enum led_brightness value);
};

static inline void delayed_set_status_worker(struct work_struct *work)
{
	struct delayed_led_classdev *data =
			container_of(work, struct delayed_led_classdev, work);

	data->set_brightness(data, data->new_brightness);
}

static inline void delayed_sysfs_set(struct led_classdev *led_cdev,
			      enum led_brightness brightness)
{
	struct delayed_led_classdev *data = container_of(led_cdev,
			     struct delayed_led_classdev, led_classdev);
	data->new_brightness = brightness;
	schedule_work(&data->work);
}

/* HP-specific accelerometer driver ------------------------------------ */

/* e0 25, e0 26, e0 27, e0 28 are scan codes that the accelerometer with acpi id
 * HPQ6000 sends through the keyboard bus */
#define ACCEL_1 0x25
#define ACCEL_2 0x26
#define ACCEL_3 0x27
#define ACCEL_4 0x28

/* For automatic insertion of the module */
static const struct acpi_device_id lis3lv02d_device_ids[] = {
	{"HPQ0004", 0}, /* HP Mobile Data Protection System PNP */
	{"HPQ6000", 0}, /* HP Mobile Data Protection System PNP */
	{"HPQ6007", 0}, /* HP Mobile Data Protection System PNP */
	{"", 0},
};
MODULE_DEVICE_TABLE(acpi, lis3lv02d_device_ids);

/**
 * lis3lv02d_acpi_init - initialize the device for ACPI
 * @lis3: pointer to the device struct
 *
 * Returns 0 on success.
 */
static int lis3lv02d_acpi_init(struct lis3lv02d *lis3)
{
	return 0;
}

/**
 * lis3lv02d_acpi_read - ACPI ALRD method: read a register
 * @lis3: pointer to the device struct
 * @reg:    the register to read
 * @ret:    result of the operation
 *
 * Returns 0 on success.
 */
static int lis3lv02d_acpi_read(struct lis3lv02d *lis3, int reg, u8 *ret)
{
	struct acpi_device *dev = lis3->bus_priv;
	union acpi_object arg0 = { ACPI_TYPE_INTEGER };
	struct acpi_object_list args = { 1, &arg0 };
	unsigned long long lret;
	acpi_status status;

	arg0.integer.value = reg;

	status = acpi_evaluate_integer(dev->handle, "ALRD", &args, &lret);
	if (ACPI_FAILURE(status))
		return -EINVAL;
	*ret = lret;
	return 0;
}

/**
 * lis3lv02d_acpi_write - ACPI ALWR method: write to a register
 * @lis3: pointer to the device struct
 * @reg:    the register to write to
 * @val:    the value to write
 *
 * Returns 0 on success.
 */
static int lis3lv02d_acpi_write(struct lis3lv02d *lis3, int reg, u8 val)
{
	struct acpi_device *dev = lis3->bus_priv;
	unsigned long long ret; /* Not used when writting */
	union acpi_object in_obj[2];
	struct acpi_object_list args = { 2, in_obj };

	in_obj[0].type          = ACPI_TYPE_INTEGER;
	in_obj[0].integer.value = reg;
	in_obj[1].type          = ACPI_TYPE_INTEGER;
	in_obj[1].integer.value = val;

	if (acpi_evaluate_integer(dev->handle, "ALWR", &args, &ret) != AE_OK)
		return -EINVAL;

	return 0;
}

static int lis3lv02d_dmi_matched(const struct dmi_system_id *dmi)
{
	lis3_dev.ac = *((union axis_conversion *)dmi->driver_data);
	pr_info("hardware type %s found\n", dmi->ident);

	return 1;
}

/* Represents, for each axis seen by userspace, the corresponding hw axis (+1).
 * If the value is negative, the opposite of the hw value is used. */
#define DEFINE_CONV(name, x, y, z)			      \
	static union axis_conversion lis3lv02d_axis_##name = \
		{ .as_array = { x, y, z } }
DEFINE_CONV(normal, 1, 2, 3);
DEFINE_CONV(y_inverted, 1, -2, 3);
DEFINE_CONV(x_inverted, -1, 2, 3);
DEFINE_CONV(x_inverted_usd, -1, 2, -3);
DEFINE_CONV(z_inverted, 1, 2, -3);
DEFINE_CONV(xy_swap, 2, 1, 3);
DEFINE_CONV(xy_rotated_left, -2, 1, 3);
DEFINE_CONV(xy_rotated_left_usd, -2, 1, -3);
DEFINE_CONV(xy_swap_inverted, -2, -1, 3);
DEFINE_CONV(xy_rotated_right, 2, -1, 3);
DEFINE_CONV(xy_swap_yz_inverted, 2, -1, -3);

#define AXIS_DMI_MATCH(_ident, _name, _axis) {		\
	.ident = _ident,				\
	.callback = lis3lv02d_dmi_matched,		\
	.matches = {					\
		DMI_MATCH(DMI_PRODUCT_NAME, _name)	\
	},						\
	.driver_data = &lis3lv02d_axis_##_axis		\
}

#define AXIS_DMI_MATCH2(_ident, _class1, _name1,	\
				_class2, _name2,	\
				_axis) {		\
	.ident = _ident,				\
	.callback = lis3lv02d_dmi_matched,		\
	.matches = {					\
		DMI_MATCH(DMI_##_class1, _name1),	\
		DMI_MATCH(DMI_##_class2, _name2),	\
	},						\
	.driver_data = &lis3lv02d_axis_##_axis		\
}
static const struct dmi_system_id lis3lv02d_dmi_ids[] = {
	/* product names are truncated to match all kinds of a same model */
	AXIS_DMI_MATCH("NC64x0", "HP Compaq nc64", x_inverted),
	AXIS_DMI_MATCH("NC84x0", "HP Compaq nc84", z_inverted),
	AXIS_DMI_MATCH("NX9420", "HP Compaq nx9420", x_inverted),
	AXIS_DMI_MATCH("NW9440", "HP Compaq nw9440", x_inverted),
	AXIS_DMI_MATCH("NC2510", "HP Compaq 2510", y_inverted),
	AXIS_DMI_MATCH("NC2710", "HP Compaq 2710", xy_swap),
	AXIS_DMI_MATCH("NC8510", "HP Compaq 8510", xy_swap_inverted),
	AXIS_DMI_MATCH("HP2133", "HP 2133", xy_rotated_left),
	AXIS_DMI_MATCH("HP2140", "HP 2140", xy_swap_inverted),
	AXIS_DMI_MATCH("NC653x", "HP Compaq 653", xy_rotated_left_usd),
	AXIS_DMI_MATCH("NC6730b", "HP Compaq 6730b", xy_rotated_left_usd),
	AXIS_DMI_MATCH("NC6730s", "HP Compaq 6730s", xy_swap),
	AXIS_DMI_MATCH("NC651xx", "HP Compaq 651", xy_rotated_right),
	AXIS_DMI_MATCH("NC6710x", "HP Compaq 6710", xy_swap_yz_inverted),
	AXIS_DMI_MATCH("NC6715x", "HP Compaq 6715", y_inverted),
	AXIS_DMI_MATCH("NC693xx", "HP EliteBook 693", xy_rotated_right),
	AXIS_DMI_MATCH("NC693xx", "HP EliteBook 853", xy_swap),
	AXIS_DMI_MATCH("NC854xx", "HP EliteBook 854", y_inverted),
	AXIS_DMI_MATCH("NC273xx", "HP EliteBook 273", y_inverted),
	/* Intel-based HP Pavilion dv5 */
	AXIS_DMI_MATCH2("HPDV5_I",
			PRODUCT_NAME, "HP Pavilion dv5",
			BOARD_NAME, "3603",
			x_inverted),
	/* AMD-based HP Pavilion dv5 */
	AXIS_DMI_MATCH2("HPDV5_A",
			PRODUCT_NAME, "HP Pavilion dv5",
			BOARD_NAME, "3600",
			y_inverted),
	AXIS_DMI_MATCH("DV7", "HP Pavilion dv7", x_inverted),
	AXIS_DMI_MATCH("HP8710", "HP Compaq 8710", y_inverted),
	AXIS_DMI_MATCH("HDX18", "HP HDX 18", x_inverted),
	AXIS_DMI_MATCH("HPB432x", "HP ProBook 432", xy_rotated_left),
	AXIS_DMI_MATCH("HPB440G3", "HP ProBook 440 G3", x_inverted_usd),
	AXIS_DMI_MATCH("HPB440G4", "HP ProBook 440 G4", x_inverted),
	AXIS_DMI_MATCH("HPB442x", "HP ProBook 442", xy_rotated_left),
	AXIS_DMI_MATCH("HPB450G0", "HP ProBook 450 G0", x_inverted),
	AXIS_DMI_MATCH("HPB452x", "HP ProBook 452", y_inverted),
	AXIS_DMI_MATCH("HPB522x", "HP ProBook 522", xy_swap),
	AXIS_DMI_MATCH("HPB532x", "HP ProBook 532", y_inverted),
	AXIS_DMI_MATCH("HPB655x", "HP ProBook 655", xy_swap_inverted),
	AXIS_DMI_MATCH("Mini510x", "HP Mini 510", xy_rotated_left_usd),
	AXIS_DMI_MATCH("HPB63xx", "HP ProBook 63", xy_swap),
	AXIS_DMI_MATCH("HPB64xx", "HP ProBook 64", xy_swap),
	AXIS_DMI_MATCH("HPB64xx", "HP EliteBook 84", xy_swap),
	AXIS_DMI_MATCH("HPB65xx", "HP ProBook 65", x_inverted),
	AXIS_DMI_MATCH("HPZBook15", "HP ZBook 15", x_inverted),
	AXIS_DMI_MATCH("HPZBook17G5", "HP ZBook 17 G5", x_inverted),
	AXIS_DMI_MATCH("HPZBook17", "HP ZBook 17", xy_swap_yz_inverted),
	{ NULL, }
/* Laptop models without axis info (yet):
 * "NC6910" "HP Compaq 6910"
 * "NC2400" "HP Compaq nc2400"
 * "NX74x0" "HP Compaq nx74"
 * "NX6325" "HP Compaq nx6325"
 * "NC4400" "HP Compaq nc4400"
 */
};

static void hpled_set(struct delayed_led_classdev *led_cdev, enum led_brightness value)
{
	struct acpi_device *dev = lis3_dev.bus_priv;
	unsigned long long ret; /* Not used when writing */
	union acpi_object in_obj[1];
	struct acpi_object_list args = { 1, in_obj };

	in_obj[0].type          = ACPI_TYPE_INTEGER;
	in_obj[0].integer.value = !!value;

	acpi_evaluate_integer(dev->handle, "ALED", &args, &ret);
}

static struct delayed_led_classdev hpled_led = {
	.led_classdev = {
		.name			= "hp::hddprotect",
		.default_trigger	= "none",
		.brightness_set		= delayed_sysfs_set,
		.flags                  = LED_CORE_SUSPENDRESUME,
	},
	.set_brightness = hpled_set,
};

static acpi_status
lis3lv02d_get_resource(struct acpi_resource *resource, void *context)
{
	if (resource->type == ACPI_RESOURCE_TYPE_EXTENDED_IRQ) {
		struct acpi_resource_extended_irq *irq;
		u32 *device_irq = context;

		irq = &resource->data.extended_irq;
		*device_irq = irq->interrupts[0];
	}

	return AE_OK;
}

static void lis3lv02d_enum_resources(struct acpi_device *device)
{
	acpi_status status;

	status = acpi_walk_resources(device->handle, METHOD_NAME__CRS,
					lis3lv02d_get_resource, &lis3_dev.irq);
	if (ACPI_FAILURE(status))
		printk(KERN_DEBUG DRIVER_NAME ": Error getting resources\n");
}

static bool hp_accel_i8042_filter(unsigned char data, unsigned char str,
				  struct serio *port)
{
	static bool extended;

	if (str & I8042_STR_AUXDATA)
		return false;

	if (data == 0xe0) {
		extended = true;
		return true;
	} else if (unlikely(extended)) {
		extended = false;

		switch (data) {
		case ACCEL_1:
		case ACCEL_2:
		case ACCEL_3:
		case ACCEL_4:
			return true;
		default:
			serio_interrupt(port, 0xe0, 0);
			return false;
		}
	}

	return false;
}

static int lis3lv02d_add(struct acpi_device *device)
{
	int ret;

	if (!device)
		return -EINVAL;

	lis3_dev.bus_priv = device;
	lis3_dev.init = lis3lv02d_acpi_init;
	lis3_dev.read = lis3lv02d_acpi_read;
	lis3_dev.write = lis3lv02d_acpi_write;
	strcpy(acpi_device_name(device), DRIVER_NAME);
	strcpy(acpi_device_class(device), ACPI_MDPS_CLASS);
	device->driver_data = &lis3_dev;

	/* obtain IRQ number of our device from ACPI */
	lis3lv02d_enum_resources(device);

	/* If possible use a "standard" axes order */
	if (lis3_dev.ac.x && lis3_dev.ac.y && lis3_dev.ac.z) {
		pr_info("Using custom axes %d,%d,%d\n",
			lis3_dev.ac.x, lis3_dev.ac.y, lis3_dev.ac.z);
	} else if (dmi_check_system(lis3lv02d_dmi_ids) == 0) {
		pr_info("laptop model unknown, using default axes configuration\n");
		lis3_dev.ac = lis3lv02d_axis_normal;
	}

	/* call the core layer do its init */
	ret = lis3lv02d_init_device(&lis3_dev);
	if (ret)
		return ret;

	/* filter to remove HPQ6000 accelerometer data
	 * from keyboard bus stream */
	if (strstr(dev_name(&device->dev), "HPQ6000"))
		i8042_install_filter(hp_accel_i8042_filter);

	INIT_WORK(&hpled_led.work, delayed_set_status_worker);
	ret = led_classdev_register(NULL, &hpled_led.led_classdev);
	if (ret) {
		lis3lv02d_joystick_disable(&lis3_dev);
		lis3lv02d_poweroff(&lis3_dev);
		flush_work(&hpled_led.work);
		return ret;
	}

	return ret;
}

static int lis3lv02d_remove(struct acpi_device *device)
{
	if (!device)
		return -EINVAL;

	i8042_remove_filter(hp_accel_i8042_filter);
	lis3lv02d_joystick_disable(&lis3_dev);
	lis3lv02d_poweroff(&lis3_dev);

	led_classdev_unregister(&hpled_led.led_classdev);
	flush_work(&hpled_led.work);

	return lis3lv02d_remove_fs(&lis3_dev);
}


#ifdef CONFIG_PM_SLEEP
static int lis3lv02d_suspend(struct device *dev)
{
	/* make sure the device is off when we suspend */
	lis3lv02d_poweroff(&lis3_dev);
	return 0;
}

static int lis3lv02d_resume(struct device *dev)
{
	lis3lv02d_poweron(&lis3_dev);
	return 0;
}

static int lis3lv02d_restore(struct device *dev)
{
	lis3lv02d_poweron(&lis3_dev);
	return 0;
}

static const struct dev_pm_ops hp_accel_pm = {
	.suspend = lis3lv02d_suspend,
	.resume = lis3lv02d_resume,
	.freeze = lis3lv02d_suspend,
	.thaw = lis3lv02d_resume,
	.poweroff = lis3lv02d_suspend,
	.restore = lis3lv02d_restore,
};

#define HP_ACCEL_PM (&hp_accel_pm)
#else
#define HP_ACCEL_PM NULL
#endif

/* For the HP MDPS aka 3D Driveguard */
static struct acpi_driver lis3lv02d_driver = {
	.name  = DRIVER_NAME,
	.class = ACPI_MDPS_CLASS,
	.ids   = lis3lv02d_device_ids,
	.ops = {
		.add     = lis3lv02d_add,
		.remove  = lis3lv02d_remove,
	},
	.drv.pm = HP_ACCEL_PM,
};
module_acpi_driver(lis3lv02d_driver);

MODULE_DESCRIPTION("Glue between LIS3LV02Dx and HP ACPI BIOS and support for disk protection LED.");
MODULE_AUTHOR("Yan Burman, Eric Piel, Pavel Machek");
MODULE_LICENSE("GPL");