Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

platform/surface: Add Surface Hot-Plug driver

Some Surface Book 2 and 3 models have a discrete GPU (dGPU) that is
hot-pluggable. On those devices, the dGPU is contained in the base,
which can be separated from the tablet part (containing CPU and
touchscreen) while the device is running.

It (in general) is presented as/behaves like a standard PCIe hot-plug
capable device, however, this device can also be put into D3cold. In
D3cold, the device itself is turned off and can thus not submit any
standard PCIe hot-plug events. To properly detect hot-(un)plugging while
the dGPU is in D3cold, out-of-band signaling is required. Without this,
the device state will only get updated during the next bus-check, eg.
via a manually issued lspci call.

This commit adds a driver to handle out-of-band PCIe hot-(un)plug events
on Microsoft Surface devices. On those devices, said events can be
detected via GPIO interrupts, which are then forwarded to the
corresponding ACPI DSM calls by this driver. The DSM then takes care of
issuing the appropriate bus-/device-check, causing the PCI core to
properly pick up the device change.

Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com>
Link: https://lore.kernel.org/r/20210205012657.1951753-1-luzmaximilian@gmail.com
Signed-off-by: Hans de Goede <hdegoede@redhat.com>

authored by

Maximilian Luz and committed by
Hans de Goede
bd69bcce fc4325a1

+308
+6
MAINTAINERS
··· 11805 11805 T: git git://git.kernel.org/pub/scm/linux/kernel/git/pdx86/platform-drivers-x86.git 11806 11806 F: drivers/platform/surface/ 11807 11807 11808 + MICROSOFT SURFACE HOT-PLUG DRIVER 11809 + M: Maximilian Luz <luzmaximilian@gmail.com> 11810 + L: platform-driver-x86@vger.kernel.org 11811 + S: Maintained 11812 + F: drivers/platform/surface/surface_hotplug.c 11813 + 11808 11814 MICROSOFT SURFACE PRO 3 BUTTON DRIVER 11809 11815 M: Chen Yu <yu.c.chen@intel.com> 11810 11816 L: platform-driver-x86@vger.kernel.org
+19
drivers/platform/surface/Kconfig
··· 86 86 accordingly. It is required on those devices to allow wake-ups from 87 87 suspend by opening the lid. 88 88 89 + config SURFACE_HOTPLUG 90 + tristate "Surface Hot-Plug Driver" 91 + depends on GPIOLIB 92 + help 93 + Driver for out-of-band hot-plug event signaling on Microsoft Surface 94 + devices with hot-pluggable PCIe cards. 95 + 96 + This driver is used on Surface Book (2 and 3) devices with a 97 + hot-pluggable discrete GPU (dGPU). When not in use, the dGPU on those 98 + devices can enter D3cold, which prevents in-band (standard) PCIe 99 + hot-plug signaling. Thus, without this driver, detaching the base 100 + containing the dGPU will not correctly update the state of the 101 + corresponding PCIe device if it is in D3cold. This driver adds support 102 + for out-of-band hot-plug notifications, ensuring that the device state 103 + is properly updated even when the device in question is in D3cold. 104 + 105 + Select M or Y here, if you want to (fully) support hot-plugging of 106 + dGPU devices on the Surface Book 2 and/or 3 during D3cold. 107 + 89 108 config SURFACE_PRO3_BUTTON 90 109 tristate "Power/home/volume buttons driver for Microsoft Surface Pro 3/4 tablet" 91 110 depends on INPUT
+1
drivers/platform/surface/Makefile
··· 11 11 obj-$(CONFIG_SURFACE_AGGREGATOR) += aggregator/ 12 12 obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o 13 13 obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o 14 + obj-$(CONFIG_SURFACE_HOTPLUG) += surface_hotplug.o 14 15 obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o
+282
drivers/platform/surface/surface_hotplug.c
··· 1 + // SPDX-License-Identifier: GPL-2.0+ 2 + /* 3 + * Surface Book (2 and later) hot-plug driver. 4 + * 5 + * Surface Book devices (can) have a hot-pluggable discrete GPU (dGPU). This 6 + * driver is responsible for out-of-band hot-plug event signaling on these 7 + * devices. It is specifically required when the hot-plug device is in D3cold 8 + * and can thus not generate PCIe hot-plug events itself. 9 + * 10 + * Event signaling is handled via ACPI, which will generate the appropriate 11 + * device-check notifications to be picked up by the PCIe hot-plug driver. 12 + * 13 + * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com> 14 + */ 15 + 16 + #include <linux/acpi.h> 17 + #include <linux/gpio.h> 18 + #include <linux/interrupt.h> 19 + #include <linux/kernel.h> 20 + #include <linux/module.h> 21 + #include <linux/mutex.h> 22 + #include <linux/platform_device.h> 23 + 24 + static const struct acpi_gpio_params shps_base_presence_int = { 0, 0, false }; 25 + static const struct acpi_gpio_params shps_base_presence = { 1, 0, false }; 26 + static const struct acpi_gpio_params shps_device_power_int = { 2, 0, false }; 27 + static const struct acpi_gpio_params shps_device_power = { 3, 0, false }; 28 + static const struct acpi_gpio_params shps_device_presence_int = { 4, 0, false }; 29 + static const struct acpi_gpio_params shps_device_presence = { 5, 0, false }; 30 + 31 + static const struct acpi_gpio_mapping shps_acpi_gpios[] = { 32 + { "base_presence-int-gpio", &shps_base_presence_int, 1 }, 33 + { "base_presence-gpio", &shps_base_presence, 1 }, 34 + { "device_power-int-gpio", &shps_device_power_int, 1 }, 35 + { "device_power-gpio", &shps_device_power, 1 }, 36 + { "device_presence-int-gpio", &shps_device_presence_int, 1 }, 37 + { "device_presence-gpio", &shps_device_presence, 1 }, 38 + { }, 39 + }; 40 + 41 + /* 5515a847-ed55-4b27-8352-cd320e10360a */ 42 + static const guid_t shps_dsm_guid = 43 + GUID_INIT(0x5515a847, 0xed55, 0x4b27, 0x83, 0x52, 0xcd, 0x32, 0x0e, 0x10, 0x36, 0x0a); 44 + 45 + #define SHPS_DSM_REVISION 1 46 + 47 + enum shps_dsm_fn { 48 + SHPS_DSM_FN_PCI_NUM_ENTRIES = 0x01, 49 + SHPS_DSM_FN_PCI_GET_ENTRIES = 0x02, 50 + SHPS_DSM_FN_IRQ_BASE_PRESENCE = 0x03, 51 + SHPS_DSM_FN_IRQ_DEVICE_POWER = 0x04, 52 + SHPS_DSM_FN_IRQ_DEVICE_PRESENCE = 0x05, 53 + }; 54 + 55 + enum shps_irq_type { 56 + /* NOTE: Must be in order of enum shps_dsm_fn above. */ 57 + SHPS_IRQ_TYPE_BASE_PRESENCE = 0, 58 + SHPS_IRQ_TYPE_DEVICE_POWER = 1, 59 + SHPS_IRQ_TYPE_DEVICE_PRESENCE = 2, 60 + SHPS_NUM_IRQS, 61 + }; 62 + 63 + static const char *const shps_gpio_names[] = { 64 + [SHPS_IRQ_TYPE_BASE_PRESENCE] = "base_presence", 65 + [SHPS_IRQ_TYPE_DEVICE_POWER] = "device_power", 66 + [SHPS_IRQ_TYPE_DEVICE_PRESENCE] = "device_presence", 67 + }; 68 + 69 + struct shps_device { 70 + struct mutex lock[SHPS_NUM_IRQS]; /* Protects update in shps_dsm_notify_irq() */ 71 + struct gpio_desc *gpio[SHPS_NUM_IRQS]; 72 + unsigned int irq[SHPS_NUM_IRQS]; 73 + }; 74 + 75 + #define SHPS_IRQ_NOT_PRESENT ((unsigned int)-1) 76 + 77 + static enum shps_dsm_fn shps_dsm_fn_for_irq(enum shps_irq_type type) 78 + { 79 + return SHPS_DSM_FN_IRQ_BASE_PRESENCE + type; 80 + } 81 + 82 + static void shps_dsm_notify_irq(struct platform_device *pdev, enum shps_irq_type type) 83 + { 84 + struct shps_device *sdev = platform_get_drvdata(pdev); 85 + acpi_handle handle = ACPI_HANDLE(&pdev->dev); 86 + union acpi_object *result; 87 + union acpi_object param; 88 + int value; 89 + 90 + mutex_lock(&sdev->lock[type]); 91 + 92 + value = gpiod_get_value_cansleep(sdev->gpio[type]); 93 + if (value < 0) { 94 + mutex_unlock(&sdev->lock[type]); 95 + dev_err(&pdev->dev, "failed to get gpio: %d (irq=%d)\n", type, value); 96 + return; 97 + } 98 + 99 + dev_dbg(&pdev->dev, "IRQ notification via DSM (irq=%d, value=%d)\n", type, value); 100 + 101 + param.type = ACPI_TYPE_INTEGER; 102 + param.integer.value = value; 103 + 104 + result = acpi_evaluate_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, 105 + shps_dsm_fn_for_irq(type), &param); 106 + 107 + if (!result) { 108 + dev_err(&pdev->dev, "IRQ notification via DSM failed (irq=%d, gpio=%d)\n", 109 + type, value); 110 + 111 + } else if (result->type != ACPI_TYPE_BUFFER) { 112 + dev_err(&pdev->dev, 113 + "IRQ notification via DSM failed: unexpected result type (irq=%d, gpio=%d)\n", 114 + type, value); 115 + 116 + } else if (result->buffer.length != 1 || result->buffer.pointer[0] != 0) { 117 + dev_err(&pdev->dev, 118 + "IRQ notification via DSM failed: unexpected result value (irq=%d, gpio=%d)\n", 119 + type, value); 120 + } 121 + 122 + mutex_unlock(&sdev->lock[type]); 123 + 124 + if (result) 125 + ACPI_FREE(result); 126 + } 127 + 128 + static irqreturn_t shps_handle_irq(int irq, void *data) 129 + { 130 + struct platform_device *pdev = data; 131 + struct shps_device *sdev = platform_get_drvdata(pdev); 132 + int type; 133 + 134 + /* Figure out which IRQ we're handling. */ 135 + for (type = 0; type < SHPS_NUM_IRQS; type++) 136 + if (irq == sdev->irq[type]) 137 + break; 138 + 139 + /* We should have found our interrupt, if not: this is a bug. */ 140 + if (WARN(type >= SHPS_NUM_IRQS, "invalid IRQ number: %d\n", irq)) 141 + return IRQ_HANDLED; 142 + 143 + /* Forward interrupt to ACPI via DSM. */ 144 + shps_dsm_notify_irq(pdev, type); 145 + return IRQ_HANDLED; 146 + } 147 + 148 + static int shps_setup_irq(struct platform_device *pdev, enum shps_irq_type type) 149 + { 150 + unsigned long flags = IRQF_ONESHOT | IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING; 151 + struct shps_device *sdev = platform_get_drvdata(pdev); 152 + struct gpio_desc *gpiod; 153 + acpi_handle handle = ACPI_HANDLE(&pdev->dev); 154 + const char *irq_name; 155 + const int dsm = shps_dsm_fn_for_irq(type); 156 + int status, irq; 157 + 158 + /* 159 + * Only set up interrupts that we actually need: The Surface Book 3 160 + * does not have a DSM for base presence, so don't set up an interrupt 161 + * for that. 162 + */ 163 + if (!acpi_check_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, BIT(dsm))) { 164 + dev_dbg(&pdev->dev, "IRQ notification via DSM not present (irq=%d)\n", type); 165 + return 0; 166 + } 167 + 168 + gpiod = devm_gpiod_get(&pdev->dev, shps_gpio_names[type], GPIOD_ASIS); 169 + if (IS_ERR(gpiod)) 170 + return PTR_ERR(gpiod); 171 + 172 + irq = gpiod_to_irq(gpiod); 173 + if (irq < 0) 174 + return irq; 175 + 176 + irq_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "shps-irq-%d", type); 177 + if (!irq_name) 178 + return -ENOMEM; 179 + 180 + status = devm_request_threaded_irq(&pdev->dev, irq, NULL, shps_handle_irq, 181 + flags, irq_name, pdev); 182 + if (status) 183 + return status; 184 + 185 + dev_dbg(&pdev->dev, "set up irq %d as type %d\n", irq, type); 186 + 187 + sdev->gpio[type] = gpiod; 188 + sdev->irq[type] = irq; 189 + 190 + return 0; 191 + } 192 + 193 + static int surface_hotplug_remove(struct platform_device *pdev) 194 + { 195 + struct shps_device *sdev = platform_get_drvdata(pdev); 196 + int i; 197 + 198 + /* Ensure that IRQs have been fully handled and won't trigger any more. */ 199 + for (i = 0; i < SHPS_NUM_IRQS; i++) { 200 + if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) 201 + disable_irq(sdev->irq[i]); 202 + 203 + mutex_destroy(&sdev->lock[i]); 204 + } 205 + 206 + return 0; 207 + } 208 + 209 + static int surface_hotplug_probe(struct platform_device *pdev) 210 + { 211 + struct shps_device *sdev; 212 + int status, i; 213 + 214 + /* 215 + * The MSHW0153 device is also present on the Surface Laptop 3, 216 + * however that doesn't have a hot-pluggable PCIe device. It also 217 + * doesn't have any GPIO interrupts/pins under the MSHW0153, so filter 218 + * it out here. 219 + */ 220 + if (gpiod_count(&pdev->dev, NULL) < 0) 221 + return -ENODEV; 222 + 223 + status = devm_acpi_dev_add_driver_gpios(&pdev->dev, shps_acpi_gpios); 224 + if (status) 225 + return status; 226 + 227 + sdev = devm_kzalloc(&pdev->dev, sizeof(*sdev), GFP_KERNEL); 228 + if (!sdev) 229 + return -ENOMEM; 230 + 231 + platform_set_drvdata(pdev, sdev); 232 + 233 + /* 234 + * Initialize IRQs so that we can safely call surface_hotplug_remove() 235 + * on errors. 236 + */ 237 + for (i = 0; i < SHPS_NUM_IRQS; i++) 238 + sdev->irq[i] = SHPS_IRQ_NOT_PRESENT; 239 + 240 + /* Set up IRQs. */ 241 + for (i = 0; i < SHPS_NUM_IRQS; i++) { 242 + mutex_init(&sdev->lock[i]); 243 + 244 + status = shps_setup_irq(pdev, i); 245 + if (status) { 246 + dev_err(&pdev->dev, "failed to set up IRQ %d: %d\n", i, status); 247 + goto err; 248 + } 249 + } 250 + 251 + /* Ensure everything is up-to-date. */ 252 + for (i = 0; i < SHPS_NUM_IRQS; i++) 253 + if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) 254 + shps_dsm_notify_irq(pdev, i); 255 + 256 + return 0; 257 + 258 + err: 259 + surface_hotplug_remove(pdev); 260 + return status; 261 + } 262 + 263 + static const struct acpi_device_id surface_hotplug_acpi_match[] = { 264 + { "MSHW0153", 0 }, 265 + { }, 266 + }; 267 + MODULE_DEVICE_TABLE(acpi, surface_hotplug_acpi_match); 268 + 269 + static struct platform_driver surface_hotplug_driver = { 270 + .probe = surface_hotplug_probe, 271 + .remove = surface_hotplug_remove, 272 + .driver = { 273 + .name = "surface_hotplug", 274 + .acpi_match_table = surface_hotplug_acpi_match, 275 + .probe_type = PROBE_PREFER_ASYNCHRONOUS, 276 + }, 277 + }; 278 + module_platform_driver(surface_hotplug_driver); 279 + 280 + MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); 281 + MODULE_DESCRIPTION("Surface Hot-Plug Signaling Driver for Surface Book Devices"); 282 + MODULE_LICENSE("GPL");