aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/platform/surface/surface_hotplug.c
blob: f004a24952013fa0127eb42ea398e0b61a1dc1df (plain) (blame)
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
// SPDX-License-Identifier: GPL-2.0+
/*
 * Surface Book (2 and later) hot-plug driver.
 *
 * Surface Book devices (can) have a hot-pluggable discrete GPU (dGPU). This
 * driver is responsible for out-of-band hot-plug event signaling on these
 * devices. It is specifically required when the hot-plug device is in D3cold
 * and can thus not generate PCIe hot-plug events itself.
 *
 * Event signaling is handled via ACPI, which will generate the appropriate
 * device-check notifications to be picked up by the PCIe hot-plug driver.
 *
 * Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
 */

#include <linux/acpi.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>

static const struct acpi_gpio_params shps_base_presence_int   = { 0, 0, false };
static const struct acpi_gpio_params shps_base_presence       = { 1, 0, false };
static const struct acpi_gpio_params shps_device_power_int    = { 2, 0, false };
static const struct acpi_gpio_params shps_device_power        = { 3, 0, false };
static const struct acpi_gpio_params shps_device_presence_int = { 4, 0, false };
static const struct acpi_gpio_params shps_device_presence     = { 5, 0, false };

static const struct acpi_gpio_mapping shps_acpi_gpios[] = {
	{ "base_presence-int-gpio",   &shps_base_presence_int,   1 },
	{ "base_presence-gpio",       &shps_base_presence,       1 },
	{ "device_power-int-gpio",    &shps_device_power_int,    1 },
	{ "device_power-gpio",        &shps_device_power,        1 },
	{ "device_presence-int-gpio", &shps_device_presence_int, 1 },
	{ "device_presence-gpio",     &shps_device_presence,     1 },
	{ },
};

/* 5515a847-ed55-4b27-8352-cd320e10360a */
static const guid_t shps_dsm_guid =
	GUID_INIT(0x5515a847, 0xed55, 0x4b27, 0x83, 0x52, 0xcd, 0x32, 0x0e, 0x10, 0x36, 0x0a);

#define SHPS_DSM_REVISION		1

enum shps_dsm_fn {
	SHPS_DSM_FN_PCI_NUM_ENTRIES	= 0x01,
	SHPS_DSM_FN_PCI_GET_ENTRIES	= 0x02,
	SHPS_DSM_FN_IRQ_BASE_PRESENCE	= 0x03,
	SHPS_DSM_FN_IRQ_DEVICE_POWER	= 0x04,
	SHPS_DSM_FN_IRQ_DEVICE_PRESENCE	= 0x05,
};

enum shps_irq_type {
	/* NOTE: Must be in order of enum shps_dsm_fn above. */
	SHPS_IRQ_TYPE_BASE_PRESENCE	= 0,
	SHPS_IRQ_TYPE_DEVICE_POWER	= 1,
	SHPS_IRQ_TYPE_DEVICE_PRESENCE	= 2,
	SHPS_NUM_IRQS,
};

static const char *const shps_gpio_names[] = {
	[SHPS_IRQ_TYPE_BASE_PRESENCE]	= "base_presence",
	[SHPS_IRQ_TYPE_DEVICE_POWER]	= "device_power",
	[SHPS_IRQ_TYPE_DEVICE_PRESENCE]	= "device_presence",
};

struct shps_device {
	struct mutex lock[SHPS_NUM_IRQS];  /* Protects update in shps_dsm_notify_irq() */
	struct gpio_desc *gpio[SHPS_NUM_IRQS];
	unsigned int irq[SHPS_NUM_IRQS];
};

#define SHPS_IRQ_NOT_PRESENT		((unsigned int)-1)

static enum shps_dsm_fn shps_dsm_fn_for_irq(enum shps_irq_type type)
{
	return SHPS_DSM_FN_IRQ_BASE_PRESENCE + type;
}

static void shps_dsm_notify_irq(struct platform_device *pdev, enum shps_irq_type type)
{
	struct shps_device *sdev = platform_get_drvdata(pdev);
	acpi_handle handle = ACPI_HANDLE(&pdev->dev);
	union acpi_object *result;
	union acpi_object param;
	int value;

	mutex_lock(&sdev->lock[type]);

	value = gpiod_get_value_cansleep(sdev->gpio[type]);
	if (value < 0) {
		mutex_unlock(&sdev->lock[type]);
		dev_err(&pdev->dev, "failed to get gpio: %d (irq=%d)\n", type, value);
		return;
	}

	dev_dbg(&pdev->dev, "IRQ notification via DSM (irq=%d, value=%d)\n", type, value);

	param.type = ACPI_TYPE_INTEGER;
	param.integer.value = value;

	result = acpi_evaluate_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION,
				   shps_dsm_fn_for_irq(type), &param);

	if (!result) {
		dev_err(&pdev->dev, "IRQ notification via DSM failed (irq=%d, gpio=%d)\n",
			type, value);

	} else if (result->type != ACPI_TYPE_BUFFER) {
		dev_err(&pdev->dev,
			"IRQ notification via DSM failed: unexpected result type (irq=%d, gpio=%d)\n",
			type, value);

	} else if (result->buffer.length != 1 || result->buffer.pointer[0] != 0) {
		dev_err(&pdev->dev,
			"IRQ notification via DSM failed: unexpected result value (irq=%d, gpio=%d)\n",
			type, value);
	}

	mutex_unlock(&sdev->lock[type]);

	if (result)
		ACPI_FREE(result);
}

static irqreturn_t shps_handle_irq(int irq, void *data)
{
	struct platform_device *pdev = data;
	struct shps_device *sdev = platform_get_drvdata(pdev);
	int type;

	/* Figure out which IRQ we're handling. */
	for (type = 0; type < SHPS_NUM_IRQS; type++)
		if (irq == sdev->irq[type])
			break;

	/* We should have found our interrupt, if not: this is a bug. */
	if (WARN(type >= SHPS_NUM_IRQS, "invalid IRQ number: %d\n", irq))
		return IRQ_HANDLED;

	/* Forward interrupt to ACPI via DSM. */
	shps_dsm_notify_irq(pdev, type);
	return IRQ_HANDLED;
}

static int shps_setup_irq(struct platform_device *pdev, enum shps_irq_type type)
{
	unsigned long flags = IRQF_ONESHOT | IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
	struct shps_device *sdev = platform_get_drvdata(pdev);
	struct gpio_desc *gpiod;
	acpi_handle handle = ACPI_HANDLE(&pdev->dev);
	const char *irq_name;
	const int dsm = shps_dsm_fn_for_irq(type);
	int status, irq;

	/*
	 * Only set up interrupts that we actually need: The Surface Book 3
	 * does not have a DSM for base presence, so don't set up an interrupt
	 * for that.
	 */
	if (!acpi_check_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, BIT(dsm))) {
		dev_dbg(&pdev->dev, "IRQ notification via DSM not present (irq=%d)\n", type);
		return 0;
	}

	gpiod = devm_gpiod_get(&pdev->dev, shps_gpio_names[type], GPIOD_ASIS);
	if (IS_ERR(gpiod))
		return PTR_ERR(gpiod);

	irq = gpiod_to_irq(gpiod);
	if (irq < 0)
		return irq;

	irq_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "shps-irq-%d", type);
	if (!irq_name)
		return -ENOMEM;

	status = devm_request_threaded_irq(&pdev->dev, irq, NULL, shps_handle_irq,
					   flags, irq_name, pdev);
	if (status)
		return status;

	dev_dbg(&pdev->dev, "set up irq %d as type %d\n", irq, type);

	sdev->gpio[type] = gpiod;
	sdev->irq[type] = irq;

	return 0;
}

static int surface_hotplug_remove(struct platform_device *pdev)
{
	struct shps_device *sdev = platform_get_drvdata(pdev);
	int i;

	/* Ensure that IRQs have been fully handled and won't trigger any more. */
	for (i = 0; i < SHPS_NUM_IRQS; i++) {
		if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT)
			disable_irq(sdev->irq[i]);

		mutex_destroy(&sdev->lock[i]);
	}

	return 0;
}

static int surface_hotplug_probe(struct platform_device *pdev)
{
	struct shps_device *sdev;
	int status, i;

	/*
	 * The MSHW0153 device is also present on the Surface Laptop 3,
	 * however that doesn't have a hot-pluggable PCIe device. It also
	 * doesn't have any GPIO interrupts/pins under the MSHW0153, so filter
	 * it out here.
	 */
	if (gpiod_count(&pdev->dev, NULL) < 0)
		return -ENODEV;

	status = devm_acpi_dev_add_driver_gpios(&pdev->dev, shps_acpi_gpios);
	if (status)
		return status;

	sdev = devm_kzalloc(&pdev->dev, sizeof(*sdev), GFP_KERNEL);
	if (!sdev)
		return -ENOMEM;

	platform_set_drvdata(pdev, sdev);

	/*
	 * Initialize IRQs so that we can safely call surface_hotplug_remove()
	 * on errors.
	 */
	for (i = 0; i < SHPS_NUM_IRQS; i++)
		sdev->irq[i] = SHPS_IRQ_NOT_PRESENT;

	/* Set up IRQs. */
	for (i = 0; i < SHPS_NUM_IRQS; i++) {
		mutex_init(&sdev->lock[i]);

		status = shps_setup_irq(pdev, i);
		if (status) {
			dev_err(&pdev->dev, "failed to set up IRQ %d: %d\n", i, status);
			goto err;
		}
	}

	/* Ensure everything is up-to-date. */
	for (i = 0; i < SHPS_NUM_IRQS; i++)
		if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT)
			shps_dsm_notify_irq(pdev, i);

	return 0;

err:
	surface_hotplug_remove(pdev);
	return status;
}

static const struct acpi_device_id surface_hotplug_acpi_match[] = {
	{ "MSHW0153", 0 },
	{ },
};
MODULE_DEVICE_TABLE(acpi, surface_hotplug_acpi_match);

static struct platform_driver surface_hotplug_driver = {
	.probe = surface_hotplug_probe,
	.remove = surface_hotplug_remove,
	.driver = {
		.name = "surface_hotplug",
		.acpi_match_table = surface_hotplug_acpi_match,
		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
	},
};
module_platform_driver(surface_hotplug_driver);

MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Surface Hot-Plug Signaling Driver for Surface Book Devices");
MODULE_LICENSE("GPL");