Loading...
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-2021 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), ¶m);
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");
|