diff options
Diffstat (limited to 'drivers/platform/surface')
-rw-r--r-- | drivers/platform/surface/Kconfig | 69 | ||||
-rw-r--r-- | drivers/platform/surface/Makefile | 3 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/controller.c | 16 | ||||
-rw-r--r-- | drivers/platform/surface/surface_aggregator_registry.c | 626 | ||||
-rw-r--r-- | drivers/platform/surface/surface_dtx.c | 1289 | ||||
-rw-r--r-- | drivers/platform/surface/surface_platform_profile.c | 190 | ||||
-rw-r--r-- | drivers/platform/surface/surfacepro3_button.c | 2 |
7 files changed, 2185 insertions, 10 deletions
diff --git a/drivers/platform/surface/Kconfig b/drivers/platform/surface/Kconfig index 0847b2dc97bf..3105f651614f 100644 --- a/drivers/platform/surface/Kconfig +++ b/drivers/platform/surface/Kconfig @@ -77,6 +77,53 @@ config SURFACE_AGGREGATOR_CDEV The provided interface is intended for debugging and development only, and should not be used otherwise. +config SURFACE_AGGREGATOR_REGISTRY + tristate "Surface System Aggregator Module Device Registry" + depends on SURFACE_AGGREGATOR + depends on SURFACE_AGGREGATOR_BUS + help + Device-registry and device-hubs for Surface System Aggregator Module + (SSAM) devices. + + Provides a module and driver which act as a device-registry for SSAM + client devices that cannot be detected automatically, e.g. via ACPI. + Such devices are instead provided via this registry and attached via + device hubs, also provided in this module. + + Devices provided via this registry are: + - Platform profile (performance-/cooling-mode) device (5th- and later + generations). + - Battery/AC devices (7th-generation). + - HID input devices (7th-generation). + + Select M (recommended) or Y here if you want support for the above + mentioned devices on the corresponding Surface models. Without this + module, the respective devices will not be instantiated and thus any + functionality provided by them will be missing, even when drivers for + these devices are present. In other words, this module only provides + the respective client devices. Drivers for these devices still need to + be selected via the other options. + +config SURFACE_DTX + tristate "Surface DTX (Detachment System) Driver" + depends on SURFACE_AGGREGATOR + depends on INPUT + help + Driver for the Surface Book clipboard detachment system (DTX). + + On the Surface Book series devices, the display part containing the + CPU (called the clipboard) can be detached from the base (containing a + battery, the keyboard, and, optionally, a discrete GPU) by (if + necessary) unlocking and opening the latch connecting both parts. + + This driver provides a user-space interface that can influence the + behavior of this process, which includes the option to abort it in + case the base is still in use or speed it up in case it is not. + + Note that this module can be built without support for the Surface + Aggregator Bus (i.e. CONFIG_SURFACE_AGGREGATOR_BUS=n). In that case, + some devices, specifically the Surface Book 3, will not be supported. + config SURFACE_GPE tristate "Surface GPE/Lid Support Driver" depends on DMI @@ -105,6 +152,28 @@ config SURFACE_HOTPLUG Select M or Y here, if you want to (fully) support hot-plugging of dGPU devices on the Surface Book 2 and/or 3 during D3cold. +config SURFACE_PLATFORM_PROFILE + tristate "Surface Platform Profile Driver" + depends on SURFACE_AGGREGATOR_REGISTRY + select ACPI_PLATFORM_PROFILE + help + Provides support for the ACPI platform profile on 5th- and later + generation Microsoft Surface devices. + + More specifically, this driver provides ACPI platform profile support + on Microsoft Surface devices with a Surface System Aggregator Module + (SSAM) connected via the Surface Serial Hub (SSH / SAM-over-SSH). In + other words, this driver provides platform profile support on the + Surface Pro 5, Surface Book 2, Surface Laptop, Surface Laptop Go and + later. On those devices, the platform profile can significantly + influence cooling behavior, e.g. setting it to 'quiet' (default) or + 'low-power' can significantly limit performance of the discrete GPU on + Surface Books, while in turn leading to lower power consumption and/or + less fan noise. + + Select M or Y here, if you want to include ACPI platform profile + support on the above mentioned devices. + config SURFACE_PRO3_BUTTON tristate "Power/home/volume buttons driver for Microsoft Surface Pro 3/4 tablet" depends on INPUT diff --git a/drivers/platform/surface/Makefile b/drivers/platform/surface/Makefile index 990424c5f0c9..32889482de55 100644 --- a/drivers/platform/surface/Makefile +++ b/drivers/platform/surface/Makefile @@ -10,6 +10,9 @@ obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o obj-$(CONFIG_SURFACE_ACPI_NOTIFY) += surface_acpi_notify.o obj-$(CONFIG_SURFACE_AGGREGATOR) += aggregator/ obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o +obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o +obj-$(CONFIG_SURFACE_DTX) += surface_dtx.o obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o obj-$(CONFIG_SURFACE_HOTPLUG) += surface_hotplug.o +obj-$(CONFIG_SURFACE_PLATFORM_PROFILE) += surface_platform_profile.o obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o diff --git a/drivers/platform/surface/aggregator/controller.c b/drivers/platform/surface/aggregator/controller.c index 5bcb59ed579d..69e86cd599d3 100644 --- a/drivers/platform/surface/aggregator/controller.c +++ b/drivers/platform/surface/aggregator/controller.c @@ -1040,7 +1040,7 @@ static int ssam_dsm_load_u32(acpi_handle handle, u64 funcs, u64 func, u32 *ret) union acpi_object *obj; u64 val; - if (!(funcs & BIT(func))) + if (!(funcs & BIT_ULL(func))) return 0; /* Not supported, leave *ret at its default value */ obj = acpi_evaluate_dsm_typed(handle, &SSAM_SSH_DSM_GUID, @@ -1750,35 +1750,35 @@ EXPORT_SYMBOL_GPL(ssam_request_sync_with_buffer); /* -- Internal SAM requests. ------------------------------------------------ */ -static SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_get_firmware_version, __le32, { +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_get_firmware_version, __le32, { .target_category = SSAM_SSH_TC_SAM, .target_id = 0x01, .command_id = 0x13, .instance_id = 0x00, }); -static SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_off, u8, { +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_off, u8, { .target_category = SSAM_SSH_TC_SAM, .target_id = 0x01, .command_id = 0x15, .instance_id = 0x00, }); -static SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_on, u8, { +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_on, u8, { .target_category = SSAM_SSH_TC_SAM, .target_id = 0x01, .command_id = 0x16, .instance_id = 0x00, }); -static SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_exit, u8, { +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_exit, u8, { .target_category = SSAM_SSH_TC_SAM, .target_id = 0x01, .command_id = 0x33, .instance_id = 0x00, }); -static SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_entry, u8, { +SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_entry, u8, { .target_category = SSAM_SSH_TC_SAM, .target_id = 0x01, .command_id = 0x34, @@ -2483,7 +2483,8 @@ int ssam_irq_setup(struct ssam_controller *ctrl) * interrupt, and let the SAM resume callback during the controller * resume process clear it. */ - const int irqf = IRQF_SHARED | IRQF_ONESHOT | IRQF_TRIGGER_RISING; + const int irqf = IRQF_SHARED | IRQF_ONESHOT | + IRQF_TRIGGER_RISING | IRQF_NO_AUTOEN; gpiod = gpiod_get(dev, "ssam_wakeup-int", GPIOD_ASIS); if (IS_ERR(gpiod)) @@ -2501,7 +2502,6 @@ int ssam_irq_setup(struct ssam_controller *ctrl) return status; ctrl->irq.num = irq; - disable_irq(ctrl->irq.num); return 0; } diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c new file mode 100644 index 000000000000..685d37a7add1 --- /dev/null +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface System Aggregator Module (SSAM) client device registry. + * + * Registry for non-platform/non-ACPI SSAM client devices, i.e. devices that + * cannot be auto-detected. Provides device-hubs and performs instantiation + * for these devices. + * + * Copyright (C) 2020-2021 Maximilian Luz <luzmaximilian@gmail.com> + */ + +#include <linux/acpi.h> +#include <linux/kernel.h> +#include <linux/limits.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/types.h> +#include <linux/workqueue.h> + +#include <linux/surface_aggregator/controller.h> +#include <linux/surface_aggregator/device.h> + + +/* -- Device registry. ------------------------------------------------------ */ + +/* + * SSAM device names follow the SSAM module alias, meaning they are prefixed + * with 'ssam:', followed by domain, category, target ID, instance ID, and + * function, each encoded as two-digit hexadecimal, separated by ':'. In other + * words, it follows the scheme + * + * ssam:dd:cc:tt:ii:ff + * + * Where, 'dd', 'cc', 'tt', 'ii', and 'ff' are the two-digit hexadecimal + * values mentioned above, respectively. + */ + +/* Root node. */ +static const struct software_node ssam_node_root = { + .name = "ssam_platform_hub", +}; + +/* Base device hub (devices attached to Surface Book 3 base). */ +static const struct software_node ssam_node_hub_base = { + .name = "ssam:00:00:02:00:00", + .parent = &ssam_node_root, +}; + +/* AC adapter. */ +static const struct software_node ssam_node_bat_ac = { + .name = "ssam:01:02:01:01:01", + .parent = &ssam_node_root, +}; + +/* Primary battery. */ +static const struct software_node ssam_node_bat_main = { + .name = "ssam:01:02:01:01:00", + .parent = &ssam_node_root, +}; + +/* Secondary battery (Surface Book 3). */ +static const struct software_node ssam_node_bat_sb3base = { + .name = "ssam:01:02:02:01:00", + .parent = &ssam_node_hub_base, +}; + +/* Platform profile / performance-mode device. */ +static const struct software_node ssam_node_tmp_pprof = { + .name = "ssam:01:03:01:00:01", + .parent = &ssam_node_root, +}; + +/* DTX / detachment-system device (Surface Book 3). */ +static const struct software_node ssam_node_bas_dtx = { + .name = "ssam:01:11:01:00:00", + .parent = &ssam_node_root, +}; + +/* HID keyboard. */ +static const struct software_node ssam_node_hid_main_keyboard = { + .name = "ssam:01:15:02:01:00", + .parent = &ssam_node_root, +}; + +/* HID touchpad. */ +static const struct software_node ssam_node_hid_main_touchpad = { + .name = "ssam:01:15:02:03:00", + .parent = &ssam_node_root, +}; + +/* HID device instance 5 (unknown HID device). */ +static const struct software_node ssam_node_hid_main_iid5 = { + .name = "ssam:01:15:02:05:00", + .parent = &ssam_node_root, +}; + +/* HID keyboard (base hub). */ +static const struct software_node ssam_node_hid_base_keyboard = { + .name = "ssam:01:15:02:01:00", + .parent = &ssam_node_hub_base, +}; + +/* HID touchpad (base hub). */ +static const struct software_node ssam_node_hid_base_touchpad = { + .name = "ssam:01:15:02:03:00", + .parent = &ssam_node_hub_base, +}; + +/* HID device instance 5 (unknown HID device, base hub). */ +static const struct software_node ssam_node_hid_base_iid5 = { + .name = "ssam:01:15:02:05:00", + .parent = &ssam_node_hub_base, +}; + +/* HID device instance 6 (unknown HID device, base hub). */ +static const struct software_node ssam_node_hid_base_iid6 = { + .name = "ssam:01:15:02:06:00", + .parent = &ssam_node_hub_base, +}; + +/* Devices for Surface Book 2. */ +static const struct software_node *ssam_node_group_sb2[] = { + &ssam_node_root, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Book 3. */ +static const struct software_node *ssam_node_group_sb3[] = { + &ssam_node_root, + &ssam_node_hub_base, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_bat_sb3base, + &ssam_node_tmp_pprof, + &ssam_node_bas_dtx, + &ssam_node_hid_base_keyboard, + &ssam_node_hid_base_touchpad, + &ssam_node_hid_base_iid5, + &ssam_node_hid_base_iid6, + NULL, +}; + +/* Devices for Surface Laptop 1. */ +static const struct software_node *ssam_node_group_sl1[] = { + &ssam_node_root, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Laptop 2. */ +static const struct software_node *ssam_node_group_sl2[] = { + &ssam_node_root, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Laptop 3. */ +static const struct software_node *ssam_node_group_sl3[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_pprof, + &ssam_node_hid_main_keyboard, + &ssam_node_hid_main_touchpad, + &ssam_node_hid_main_iid5, + NULL, +}; + +/* Devices for Surface Laptop Go. */ +static const struct software_node *ssam_node_group_slg1[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Pro 5. */ +static const struct software_node *ssam_node_group_sp5[] = { + &ssam_node_root, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Pro 6. */ +static const struct software_node *ssam_node_group_sp6[] = { + &ssam_node_root, + &ssam_node_tmp_pprof, + NULL, +}; + +/* Devices for Surface Pro 7 and Surface Pro 7+. */ +static const struct software_node *ssam_node_group_sp7[] = { + &ssam_node_root, + &ssam_node_bat_ac, + &ssam_node_bat_main, + &ssam_node_tmp_pprof, + NULL, +}; + + +/* -- Device registry helper functions. ------------------------------------- */ + +static int ssam_uid_from_string(const char *str, struct ssam_device_uid *uid) +{ + u8 d, tc, tid, iid, fn; + int n; + + n = sscanf(str, "ssam:%hhx:%hhx:%hhx:%hhx:%hhx", &d, &tc, &tid, &iid, &fn); + if (n != 5) + return -EINVAL; + + uid->domain = d; + uid->category = tc; + uid->target = tid; + uid->instance = iid; + uid->function = fn; + + return 0; +} + +static int ssam_hub_remove_devices_fn(struct device *dev, void *data) +{ + if (!is_ssam_device(dev)) + return 0; + + ssam_device_remove(to_ssam_device(dev)); + return 0; +} + +static void ssam_hub_remove_devices(struct device *parent) +{ + device_for_each_child_reverse(parent, NULL, ssam_hub_remove_devices_fn); +} + +static int ssam_hub_add_device(struct device *parent, struct ssam_controller *ctrl, + struct fwnode_handle *node) +{ + struct ssam_device_uid uid; + struct ssam_device *sdev; + int status; + + status = ssam_uid_from_string(fwnode_get_name(node), &uid); + if (status) + return status; + + sdev = ssam_device_alloc(ctrl, uid); + if (!sdev) + return -ENOMEM; + + sdev->dev.parent = parent; + sdev->dev.fwnode = node; + + status = ssam_device_add(sdev); + if (status) + ssam_device_put(sdev); + + return status; +} + +static int ssam_hub_add_devices(struct device *parent, struct ssam_controller *ctrl, + struct fwnode_handle *node) +{ + struct fwnode_handle *child; + int status; + + fwnode_for_each_child_node(node, child) { + /* + * Try to add the device specified in the firmware node. If + * this fails with -EINVAL, the node does not specify any SSAM + * device, so ignore it and continue with the next one. + */ + + status = ssam_hub_add_device(parent, ctrl, child); + if (status && status != -EINVAL) + goto err; + } + + return 0; +err: + ssam_hub_remove_devices(parent); + return status; +} + + +/* -- SSAM base-hub driver. ------------------------------------------------- */ + +/* + * Some devices (especially battery) may need a bit of time to be fully usable + * after being (re-)connected. This delay has been determined via + * experimentation. + */ +#define SSAM_BASE_UPDATE_CONNECT_DELAY msecs_to_jiffies(2500) + +enum ssam_base_hub_state { + SSAM_BASE_HUB_UNINITIALIZED, + SSAM_BASE_HUB_CONNECTED, + SSAM_BASE_HUB_DISCONNECTED, +}; + +struct ssam_base_hub { + struct ssam_device *sdev; + + enum ssam_base_hub_state state; + struct delayed_work update_work; + + struct ssam_event_notifier notif; +}; + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x0d, + .instance_id = 0x00, +}); + +#define SSAM_BAS_OPMODE_TABLET 0x00 +#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c + +static int ssam_base_hub_query_state(struct ssam_base_hub *hub, enum ssam_base_hub_state *state) +{ + u8 opmode; + int status; + + status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode); + if (status < 0) { + dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status); + return status; + } + + if (opmode != SSAM_BAS_OPMODE_TABLET) + *state = SSAM_BASE_HUB_CONNECTED; + else + *state = SSAM_BASE_HUB_DISCONNECTED; + + return 0; +} + +static ssize_t ssam_base_hub_state_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ssam_base_hub *hub = dev_get_drvdata(dev); + bool connected = hub->state == SSAM_BASE_HUB_CONNECTED; + + return sysfs_emit(buf, "%d\n", connected); +} + +static struct device_attribute ssam_base_hub_attr_state = + __ATTR(state, 0444, ssam_base_hub_state_show, NULL); + +static struct attribute *ssam_base_hub_attrs[] = { + &ssam_base_hub_attr_state.attr, + NULL, +}; + +static const struct attribute_group ssam_base_hub_group = { + .attrs = ssam_base_hub_attrs, +}; + +static void ssam_base_hub_update_workfn(struct work_struct *work) +{ + struct ssam_base_hub *hub = container_of(work, struct ssam_base_hub, update_work.work); + struct fwnode_handle *node = dev_fwnode(&hub->sdev->dev); + enum ssam_base_hub_state state; + int status = 0; + + status = ssam_base_hub_query_state(hub, &state); + if (status) + return; + + if (hub->state == state) + return; + hub->state = state; + + if (hub->state == SSAM_BASE_HUB_CONNECTED) + status = ssam_hub_add_devices(&hub->sdev->dev, hub->sdev->ctrl, node); + else + ssam_hub_remove_devices(&hub->sdev->dev); + + if (status) + dev_err(&hub->sdev->dev, "failed to update base-hub devices: %d\n", status); +} + +static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ + struct ssam_base_hub *hub = container_of(nf, struct ssam_base_hub, notif); + unsigned long delay; + + if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION) + return 0; + + if (event->length < 1) { + dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length); + return 0; + } + + /* + * Delay update when the base is being connected to give devices/EC + * some time to set up. + */ + delay = event->data[0] ? SSAM_BASE_UPDATE_CONNECT_DELAY : 0; + + schedule_delayed_work(&hub->update_work, delay); + + /* + * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and + * consumed by the detachment system driver. We're just a (more or less) + * silent observer. + */ + return 0; +} + +static int __maybe_unused ssam_base_hub_resume(struct device *dev) +{ + struct ssam_base_hub *hub = dev_get_drvdata(dev); + + schedule_delayed_work(&hub->update_work, 0); + return 0; +} +static SIMPLE_DEV_PM_OPS(ssam_base_hub_pm_ops, NULL, ssam_base_hub_resume); + +static int ssam_base_hub_probe(struct ssam_device *sdev) +{ + struct ssam_base_hub *hub; + int status; + + hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL); + if (!hub) + return -ENOMEM; + + hub->sdev = sdev; + hub->state = SSAM_BASE_HUB_UNINITIALIZED; + + hub->notif.base.priority = INT_MAX; /* This notifier should run first. */ + hub->notif.base.fn = ssam_base_hub_notif; + hub->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; + hub->notif.event.id.target_category = SSAM_SSH_TC_BAS, + hub->notif.event.id.instance = 0, + hub->notif.event.mask = SSAM_EVENT_MASK_NONE; + hub->notif.event.flags = SSAM_EVENT_SEQUENCED; + + INIT_DELAYED_WORK(&hub->update_work, ssam_base_hub_update_workfn); + + ssam_device_set_drvdata(sdev, hub); + + status = ssam_notifier_register(sdev->ctrl, &hub->notif); + if (status) + return status; + + status = sysfs_create_group(&sdev->dev.kobj, &ssam_base_hub_group); + if (status) + goto err; + + schedule_delayed_work(&hub->update_work, 0); + return 0; + +err: + ssam_notifier_unregister(sdev->ctrl, &hub->notif); + cancel_delayed_work_sync(&hub->update_work); + ssam_hub_remove_devices(&sdev->dev); + return status; +} + +static void ssam_base_hub_remove(struct ssam_device *sdev) +{ + struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); + + sysfs_remove_group(&sdev->dev.kobj, &ssam_base_hub_group); + + ssam_notifier_unregister(sdev->ctrl, &hub->notif); + cancel_delayed_work_sync(&hub->update_work); + ssam_hub_remove_devices(&sdev->dev); +} + +static const struct ssam_device_id ssam_base_hub_match[] = { + { SSAM_VDEV(HUB, 0x02, SSAM_ANY_IID, 0x00) }, + { }, +}; + +static struct ssam_device_driver ssam_base_hub_driver = { + .probe = ssam_base_hub_probe, + .remove = ssam_base_hub_remove, + .match_table = ssam_base_hub_match, + .driver = { + .name = "surface_aggregator_base_hub", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .pm = &ssam_base_hub_pm_ops, + }, +}; + + +/* -- SSAM platform/meta-hub driver. ---------------------------------------- */ + +static const struct acpi_device_id ssam_platform_hub_match[] = { + /* Surface Pro 4, 5, and 6 (OMBR < 0x10) */ + { "MSHW0081", (unsigned long)ssam_node_group_sp5 }, + + /* Surface Pro 6 (OMBR >= 0x10) */ + { "MSHW0111", (unsigned long)ssam_node_group_sp6 }, + + /* Surface Pro 7 */ + { "MSHW0116", (unsigned long)ssam_node_group_sp7 }, + + /* Surface Pro 7+ */ + { "MSHW0119", (unsigned long)ssam_node_group_sp7 }, + + /* Surface Book 2 */ + { "MSHW0107", (unsigned long)ssam_node_group_sb2 }, + + /* Surface Book 3 */ + { "MSHW0117", (unsigned long)ssam_node_group_sb3 }, + + /* Surface Laptop 1 */ + { "MSHW0086", (unsigned long)ssam_node_group_sl1 }, + + /* Surface Laptop 2 */ + { "MSHW0112", (unsigned long)ssam_node_group_sl2 }, + + /* Surface Laptop 3 (13", Intel) */ + { "MSHW0114", (unsigned long)ssam_node_group_sl3 }, + + /* Surface Laptop 3 (15", AMD) */ + { "MSHW0110", (unsigned long)ssam_node_group_sl3 }, + + /* Surface Laptop Go 1 */ + { "MSHW0118", (unsigned long)ssam_node_group_slg1 }, + + { }, +}; +MODULE_DEVICE_TABLE(acpi, ssam_platform_hub_match); + +static int ssam_platform_hub_probe(struct platform_device *pdev) +{ + const struct software_node **nodes; + struct ssam_controller *ctrl; + struct fwnode_handle *root; + int status; + + nodes = (const struct software_node **)acpi_device_get_match_data(&pdev->dev); + if (!nodes) + return -ENODEV; + + /* + * As we're adding the SSAM client devices as children under this device + * and not the SSAM controller, we need to add a device link to the + * controller to ensure that we remove all of our devices before the + * controller is removed. This also guarantees proper ordering for + * suspend/resume of the devices on this hub. + */ + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + status = software_node_register_node_group(nodes); + if (status) + return status; + + root = software_node_fwnode(&ssam_node_root); + if (!root) { + software_node_unregister_node_group(nodes); + return -ENOENT; + } + + set_secondary_fwnode(&pdev->dev, root); + + status = ssam_hub_add_devices(&pdev->dev, ctrl, root); + if (status) { + set_secondary_fwnode(&pdev->dev, NULL); + software_node_unregister_node_group(nodes); + } + + platform_set_drvdata(pdev, nodes); + return status; +} + +static int ssam_platform_hub_remove(struct platform_device *pdev) +{ + const struct software_node **nodes = platform_get_drvdata(pdev); + + ssam_hub_remove_devices(&pdev->dev); + set_secondary_fwnode(&pdev->dev, NULL); + software_node_unregister_node_group(nodes); + return 0; +} + +static struct platform_driver ssam_platform_hub_driver = { + .probe = ssam_platform_hub_probe, + .remove = ssam_platform_hub_remove, + .driver = { + .name = "surface_aggregator_platform_hub", + .acpi_match_table = ssam_platform_hub_match, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + + +/* -- Module initialization. ------------------------------------------------ */ + +static int __init ssam_device_hub_init(void) +{ + int status; + + status = platform_driver_register(&ssam_platform_hub_driver); + if (status) + return status; + + status = ssam_device_driver_register(&ssam_base_hub_driver); + if (status) + platform_driver_unregister(&ssam_platform_hub_driver); + + return status; +} +module_init(ssam_device_hub_init); + +static void __exit ssam_device_hub_exit(void) +{ + ssam_device_driver_unregister(&ssam_base_hub_driver); + platform_driver_unregister(&ssam_platform_hub_driver); +} +module_exit(ssam_device_hub_exit); + +MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); +MODULE_DESCRIPTION("Device-registry for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_dtx.c b/drivers/platform/surface/surface_dtx.c new file mode 100644 index 000000000000..63ce587e79e3 --- /dev/null +++ b/drivers/platform/surface/surface_dtx.c @@ -0,0 +1,1289 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Book (gen. 2 and later) detachment system (DTX) driver. + * + * Provides a user-space interface to properly handle clipboard/tablet + * (containing screen and processor) detachment from the base of the device + * (containing the keyboard and optionally a discrete GPU). Allows to + * acknowledge (to speed things up), abort (e.g. in case the dGPU is still in + * use), or request detachment via user-space. + * + * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com> + */ + +#include <linux/fs.h> +#include <linux/input.h> +#include <linux/ioctl.h> +#include <linux/kernel.h> +#include <linux/kfifo.h> +#include <linux/kref.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/poll.h> +#include <linux/rwsem.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +#include <linux/surface_aggregator/controller.h> +#include <linux/surface_aggregator/device.h> +#include <linux/surface_aggregator/dtx.h> + + +/* -- SSAM interface. ------------------------------------------------------- */ + +enum sam_event_cid_bas { + SAM_EVENT_CID_DTX_CONNECTION = 0x0c, + SAM_EVENT_CID_DTX_REQUEST = 0x0e, + SAM_EVENT_CID_DTX_CANCEL = 0x0f, + SAM_EVENT_CID_DTX_LATCH_STATUS = 0x11, +}; + +enum ssam_bas_base_state { + SSAM_BAS_BASE_STATE_DETACH_SUCCESS = 0x00, + SSAM_BAS_BASE_STATE_ATTACHED = 0x01, + SSAM_BAS_BASE_STATE_NOT_FEASIBLE = 0x02, +}; + +enum ssam_bas_latch_status { + SSAM_BAS_LATCH_STATUS_CLOSED = 0x00, + SSAM_BAS_LATCH_STATUS_OPENED = 0x01, + SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN = 0x02, + SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN = 0x03, + SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE = 0x04, +}; + +enum ssam_bas_cancel_reason { + SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE = 0x00, /* Low battery. */ + SSAM_BAS_CANCEL_REASON_TIMEOUT = 0x02, + SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN = 0x03, + SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN = 0x04, + SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE = 0x05, +}; + +struct ssam_bas_base_info { + u8 state; + u8 base_id; +} __packed; + +static_assert(sizeof(struct ssam_bas_base_info) == 2); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_lock, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x06, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_unlock, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x07, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_request, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x08, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_confirm, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x09, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_heartbeat, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x0a, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_cancel, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x0b, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_base, struct ssam_bas_base_info, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x0c, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_device_mode, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x0d, + .instance_id = 0x00, +}); + +SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_latch_status, u8, { + .target_category = SSAM_SSH_TC_BAS, + .target_id = 0x01, + .command_id = 0x11, + .instance_id = 0x00, +}); + + +/* -- Main structures. ------------------------------------------------------ */ + +enum sdtx_device_state { + SDTX_DEVICE_SHUTDOWN_BIT = BIT(0), + SDTX_DEVICE_DIRTY_BASE_BIT = BIT(1), + SDTX_DEVICE_DIRTY_MODE_BIT = BIT(2), + SDTX_DEVICE_DIRTY_LATCH_BIT = BIT(3), +}; + +struct sdtx_device { + struct kref kref; + struct rw_semaphore lock; /* Guards device and controller reference. */ + + struct device *dev; + struct ssam_controller *ctrl; + unsigned long flags; + + struct miscdevice mdev; + wait_queue_head_t waitq; + struct mutex write_lock; /* Guards order of events/notifications. */ + struct rw_semaphore client_lock; /* Guards client list. */ + struct list_head client_list; + + struct delayed_work state_work; + struct { + struct ssam_bas_base_info base; + u8 device_mode; + u8 latch_status; + } state; + + struct delayed_work mode_work; + struct input_dev *mode_switch; + + struct ssam_event_notifier notif; +}; + +enum sdtx_client_state { + SDTX_CLIENT_EVENTS_ENABLED_BIT = BIT(0), +}; + +struct sdtx_client { + struct sdtx_device *ddev; + struct list_head node; + unsigned long flags; + + struct fasync_struct *fasync; + + struct mutex read_lock; /* Guards FIFO buffer read access. */ + DECLARE_KFIFO(buffer, u8, 512); +}; + +static void __sdtx_device_release(struct kref *kref) +{ + struct sdtx_device *ddev = container_of(kref, struct sdtx_device, kref); + + mutex_destroy(&ddev->write_lock); + kfree(ddev); +} + +static struct sdtx_device *sdtx_device_get(struct sdtx_device *ddev) +{ + if (ddev) + kref_get(&ddev->kref); + + return ddev; +} + +static void sdtx_device_put(struct sdtx_device *ddev) +{ + if (ddev) + kref_put(&ddev->kref, __sdtx_device_release); +} + + +/* -- Firmware value translations. ------------------------------------------ */ + +static u16 sdtx_translate_base_state(struct sdtx_device *ddev, u8 state) +{ + switch (state) { + case SSAM_BAS_BASE_STATE_ATTACHED: + return SDTX_BASE_ATTACHED; + + case SSAM_BAS_BASE_STATE_DETACH_SUCCESS: + return SDTX_BASE_DETACHED; + + case SSAM_BAS_BASE_STATE_NOT_FEASIBLE: + return SDTX_DETACH_NOT_FEASIBLE; + + default: + dev_err(ddev->dev, "unknown base state: %#04x\n", state); + return SDTX_UNKNOWN(state); + } +} + +static u16 sdtx_translate_latch_status(struct sdtx_device *ddev, u8 status) +{ + switch (status) { + case SSAM_BAS_LATCH_STATUS_CLOSED: + return SDTX_LATCH_CLOSED; + + case SSAM_BAS_LATCH_STATUS_OPENED: + return SDTX_LATCH_OPENED; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN: + return SDTX_ERR_FAILED_TO_OPEN; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN: + return SDTX_ERR_FAILED_TO_REMAIN_OPEN; + + case SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE: + return SDTX_ERR_FAILED_TO_CLOSE; + + default: + dev_err(ddev->dev, "unknown latch status: %#04x\n", status); + return SDTX_UNKNOWN(status); + } +} + +static u16 sdtx_translate_cancel_reason(struct sdtx_device *ddev, u8 reason) +{ + switch (reason) { + case SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE: + return SDTX_DETACH_NOT_FEASIBLE; + + case SSAM_BAS_CANCEL_REASON_TIMEOUT: + return SDTX_DETACH_TIMEDOUT; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN: + return SDTX_ERR_FAILED_TO_OPEN; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN: + return SDTX_ERR_FAILED_TO_REMAIN_OPEN; + + case SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE: + return SDTX_ERR_FAILED_TO_CLOSE; + + default: + dev_err(ddev->dev, "unknown cancel reason: %#04x\n", reason); + return SDTX_UNKNOWN(reason); + } +} + + +/* -- IOCTLs. --------------------------------------------------------------- */ + +static int sdtx_ioctl_get_base_info(struct sdtx_device *ddev, + struct sdtx_base_info __user *buf) +{ + struct ssam_bas_base_info raw; + struct sdtx_base_info info; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &raw); + if (status < 0) + return status; + + info.state = sdtx_translate_base_state(ddev, raw.state); + info.base_id = SDTX_BASE_TYPE_SSH(raw.base_id); + + if (copy_to_user(buf, &info, sizeof(info))) + return -EFAULT; + + return 0; +} + +static int sdtx_ioctl_get_device_mode(struct sdtx_device *ddev, u16 __user *buf) +{ + u8 mode; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status < 0) + return status; + + return put_user(mode, buf); +} + +static int sdtx_ioctl_get_latch_status(struct sdtx_device *ddev, u16 __user *buf) +{ + u8 latch; + int status; + + lockdep_assert_held_read(&ddev->lock); + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); + if (status < 0) + return status; + + return put_user(sdtx_translate_latch_status(ddev, latch), buf); +} + +static long __surface_dtx_ioctl(struct sdtx_client *client, unsigned int cmd, unsigned long arg) +{ + struct sdtx_device *ddev = client->ddev; + + lockdep_assert_held_read(&ddev->lock); + + switch (cmd) { + case SDTX_IOCTL_EVENTS_ENABLE: + set_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); + return 0; + + case SDTX_IOCTL_EVENTS_DISABLE: + clear_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); + return 0; + + case SDTX_IOCTL_LATCH_LOCK: + return ssam_retry(ssam_bas_latch_lock, ddev->ctrl); + + case SDTX_IOCTL_LATCH_UNLOCK: + return ssam_retry(ssam_bas_latch_unlock, ddev->ctrl); + + case SDTX_IOCTL_LATCH_REQUEST: + return ssam_retry(ssam_bas_latch_request, ddev->ctrl); + + case SDTX_IOCTL_LATCH_CONFIRM: + return ssam_retry(ssam_bas_latch_confirm, ddev->ctrl); + + case SDTX_IOCTL_LATCH_HEARTBEAT: + return ssam_retry(ssam_bas_latch_heartbeat, ddev->ctrl); + + case SDTX_IOCTL_LATCH_CANCEL: + return ssam_retry(ssam_bas_latch_cancel, ddev->ctrl); + + case SDTX_IOCTL_GET_BASE_INFO: + return sdtx_ioctl_get_base_info(ddev, (struct sdtx_base_info __user *)arg); + + case SDTX_IOCTL_GET_DEVICE_MODE: + return sdtx_ioctl_get_device_mode(ddev, (u16 __user *)arg); + + case SDTX_IOCTL_GET_LATCH_STATUS: + return sdtx_ioctl_get_latch_status(ddev, (u16 __user *)arg); + + default: + return -EINVAL; + } +} + +static long surface_dtx_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct sdtx_client *client = file->private_data; + long status; + + if (down_read_killable(&client->ddev->lock)) + return -ERESTARTSYS; + + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { + up_read(&client->ddev->lock); + return -ENODEV; + } + + status = __surface_dtx_ioctl(client, cmd, arg); + + up_read(&client->ddev->lock); + return status; +} + + +/* -- File operations. ------------------------------------------------------ */ + +static int surface_dtx_open(struct inode *inode, struct file *file) +{ + struct sdtx_device *ddev = container_of(file->private_data, struct sdtx_device, mdev); + struct sdtx_client *client; + + /* Initialize client. */ + client = kzalloc(sizeof(*client), GFP_KERNEL); + if (!client) + return -ENOMEM; + + client->ddev = sdtx_device_get(ddev); + + INIT_LIST_HEAD(&client->node); + + mutex_init(&client->read_lock); + INIT_KFIFO(client->buffer); + + file->private_data = client; + + /* Attach client. */ + down_write(&ddev->client_lock); + + /* + * Do not add a new client if the device has been shut down. Note that + * it's enough to hold the client_lock here as, during shutdown, we + * only acquire that lock and remove clients after marking the device + * as shut down. + */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_write(&ddev->client_lock); + sdtx_device_put(client->ddev); + kfree(client); + return -ENODEV; + } + + list_add_tail(&client->node, &ddev->client_list); + up_write(&ddev->client_lock); + + stream_open(inode, file); + return 0; +} + +static int surface_dtx_release(struct inode *inode, struct file *file) +{ + struct sdtx_client *client = file->private_data; + + /* Detach client. */ + down_write(&client->ddev->client_lock); + list_del(&client->node); + up_write(&client->ddev->client_lock); + + /* Free client. */ + sdtx_device_put(client->ddev); + mutex_destroy(&client->read_lock); + kfree(client); + + return 0; +} + +static ssize_t surface_dtx_read(struct file *file, char __user *buf, size_t count, loff_t *offs) +{ + struct sdtx_client *client = file->private_data; + struct sdtx_device *ddev = client->ddev; + unsigned int copied; + int status = 0; + + if (down_read_killable(&ddev->lock)) + return -ERESTARTSYS; + + /* Make sure we're not shut down. */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_read(&ddev->lock); + return -ENODEV; + } + + do { + /* Check availability, wait if necessary. */ + if (kfifo_is_empty(&client->buffer)) { + up_read(&ddev->lock); + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + status = wait_event_interruptible(ddev->waitq, + !kfifo_is_empty(&client->buffer) || + test_bit(SDTX_DEVICE_SHUTDOWN_BIT, + &ddev->flags)); + if (status < 0) + return status; + + if (down_read_killable(&ddev->lock)) + return -ERESTARTSYS; + + /* Need to check that we're not shut down again. */ + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { + up_read(&ddev->lock); + return -ENODEV; + } + } + + /* Try to read from FIFO. */ + if (mutex_lock_interruptible(&client->read_lock)) { + up_read(&ddev->lock); + return -ERESTARTSYS; + } + + status = kfifo_to_user(&client->buffer, buf, count, &copied); + mutex_unlock(&client->read_lock); + + if (status < 0) { + up_read(&ddev->lock); + return status; + } + + /* We might not have gotten anything, check this here. */ + if (copied == 0 && (file->f_flags & O_NONBLOCK)) { + up_read(&ddev->lock); + return -EAGAIN; + } + } while (copied == 0); + + up_read(&ddev->lock); + return copied; +} + +static __poll_t surface_dtx_poll(struct file *file, struct poll_table_struct *pt) +{ + struct sdtx_client *client = file->private_data; + __poll_t events = 0; + + if (down_read_killable(&client->ddev->lock)) + return -ERESTARTSYS; + + if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { + up_read(&client->ddev->lock); + return EPOLLHUP | EPOLLERR; + } + + poll_wait(file, &client->ddev->waitq, pt); + + if (!kfifo_is_empty(&client->buffer)) + events |= EPOLLIN | EPOLLRDNORM; + + up_read(&client->ddev->lock); + return events; +} + +static int surface_dtx_fasync(int fd, struct file *file, int on) +{ + struct sdtx_client *client = file->private_data; + + return fasync_helper(fd, file, on, &client->fasync); +} + +static const struct file_operations surface_dtx_fops = { + .owner = THIS_MODULE, + .open = surface_dtx_open, + .release = surface_dtx_release, + .read = surface_dtx_read, + .poll = surface_dtx_poll, + .fasync = surface_dtx_fasync, + .unlocked_ioctl = surface_dtx_ioctl, + .compat_ioctl = surface_dtx_ioctl, + .llseek = no_llseek, +}; + + +/* -- Event handling/forwarding. -------------------------------------------- */ + +/* + * The device operation mode is not immediately updated on the EC when the + * base has been connected, i.e. querying the device mode inside the + * connection event callback yields an outdated value. Thus, we can only + * determine the new tablet-mode switch and device mode values after some + * time. + * + * These delays have been chosen by experimenting. We first delay on connect + * events, then check and validate the device mode against the base state and + * if invalid delay again by the "recheck" delay. + */ +#define SDTX_DEVICE_MODE_DELAY_CONNECT msecs_to_jiffies(100) +#define SDTX_DEVICE_MODE_DELAY_RECHECK msecs_to_jiffies(100) + +struct sdtx_status_event { + struct sdtx_event e; + __u16 v; +} __packed; + +struct sdtx_base_info_event { + struct sdtx_event e; + struct sdtx_base_info v; +} __packed; + +union sdtx_generic_event { + struct sdtx_event common; + struct sdtx_status_event status; + struct sdtx_base_info_event base; +}; + +static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay); + +/* Must be executed with ddev->write_lock held. */ +static void sdtx_push_event(struct sdtx_device *ddev, struct sdtx_event *evt) +{ + const size_t len = sizeof(struct sdtx_event) + evt->length; + struct sdtx_client *client; + + lockdep_assert_held(&ddev->write_lock); + + down_read(&ddev->client_lock); + list_for_each_entry(client, &ddev->client_list, node) { + if (!test_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags)) + continue; + + if (likely(kfifo_avail(&client->buffer) >= len)) + kfifo_in(&client->buffer, (const u8 *)evt, len); + else + dev_warn(ddev->dev, "event buffer overrun\n"); + + kill_fasync(&client->fasync, SIGIO, POLL_IN); + } + up_read(&ddev->client_lock); + + wake_up_interruptible(&ddev->waitq); +} + +static u32 sdtx_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in) +{ + struct sdtx_device *ddev = container_of(nf, struct sdtx_device, notif); + union sdtx_generic_event event; + size_t len; + + /* Validate event payload length. */ + switch (in->command_id) { + case SAM_EVENT_CID_DTX_CONNECTION: + len = 2 * sizeof(u8); + break; + + case SAM_EVENT_CID_DTX_REQUEST: + len = 0; + break; + + case SAM_EVENT_CID_DTX_CANCEL: + len = sizeof(u8); + break; + + case SAM_EVENT_CID_DTX_LATCH_STATUS: + len = sizeof(u8); + break; + + default: + return 0; + } + + if (in->length != len) { + dev_err(ddev->dev, + "unexpected payload size for event %#04x: got %u, expected %zu\n", + in->command_id, in->length, len); + return 0; + } + + mutex_lock(&ddev->write_lock); + + /* Translate event. */ + switch (in->command_id) { + case SAM_EVENT_CID_DTX_CONNECTION: + clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); + + /* If state has not changed: do not send new event. */ + if (ddev->state.base.state == in->data[0] && + ddev->state.base.base_id == in->data[1]) + goto out; + + ddev->state.base.state = in->data[0]; + ddev->state.base.base_id = in->data[1]; + + event.base.e.length = sizeof(struct sdtx_base_info); + event.base.e.code = SDTX_EVENT_BASE_CONNECTION; + event.base.v.state = sdtx_translate_base_state(ddev, in->data[0]); + event.base.v.base_id = SDTX_BASE_TYPE_SSH(in->data[1]); + break; + + case SAM_EVENT_CID_DTX_REQUEST: + event.common.code = SDTX_EVENT_REQUEST; + event.common.length = 0; + break; + + case SAM_EVENT_CID_DTX_CANCEL: + event.status.e.length = sizeof(u16); + event.status.e.code = SDTX_EVENT_CANCEL; + event.status.v = sdtx_translate_cancel_reason(ddev, in->data[0]); + break; + + case SAM_EVENT_CID_DTX_LATCH_STATUS: + clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); + + /* If state has not changed: do not send new event. */ + if (ddev->state.latch_status == in->data[0]) + goto out; + + ddev->state.latch_status = in->data[0]; + + event.status.e.length = sizeof(u16); + event.status.e.code = SDTX_EVENT_LATCH_STATUS; + event.status.v = sdtx_translate_latch_status(ddev, in->data[0]); + break; + } + + sdtx_push_event(ddev, &event.common); + + /* Update device mode on base connection change. */ + if (in->command_id == SAM_EVENT_CID_DTX_CONNECTION) { + unsigned long delay; + + delay = in->data[0] ? SDTX_DEVICE_MODE_DELAY_CONNECT : 0; + sdtx_update_device_mode(ddev, delay); + } + +out: + mutex_unlock(&ddev->write_lock); + return SSAM_NOTIF_HANDLED; +} + + +/* -- State update functions. ----------------------------------------------- */ + +static bool sdtx_device_mode_invalid(u8 mode, u8 base_state) +{ + return ((base_state == SSAM_BAS_BASE_STATE_ATTACHED) && + (mode == SDTX_DEVICE_MODE_TABLET)) || + ((base_state == SSAM_BAS_BASE_STATE_DETACH_SUCCESS) && + (mode != SDTX_DEVICE_MODE_TABLET)); +} + +static void sdtx_device_mode_workfn(struct work_struct *work) +{ + struct sdtx_device *ddev = container_of(work, struct sdtx_device, mode_work.work); + struct sdtx_status_event event; + struct ssam_bas_base_info base; + int status, tablet; + u8 mode; + + /* Get operation mode. */ + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status) { + dev_err(ddev->dev, "failed to get device mode: %d\n", status); + return; + } + + /* Get base info. */ + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); + if (status) { + dev_err(ddev->dev, "failed to get base info: %d\n", status); + return; + } + + /* + * In some cases (specifically when attaching the base), the device + * mode isn't updated right away. Thus we check if the device mode + * makes sense for the given base state and try again later if it + * doesn't. + */ + if (sdtx_device_mode_invalid(mode, base.state)) { + dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); + sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); + return; + } + + mutex_lock(&ddev->write_lock); + clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); + + /* Avoid sending duplicate device-mode events. */ + if (ddev->state.device_mode == mode) { + mutex_unlock(&ddev->write_lock); + return; + } + + ddev->state.device_mode = mode; + + event.e.length = sizeof(u16); + event.e.code = SDTX_EVENT_DEVICE_MODE; + event.v = mode; + + sdtx_push_event(ddev, &event.e); + + /* Send SW_TABLET_MODE event. */ + tablet = mode != SDTX_DEVICE_MODE_LAPTOP; + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); + input_sync(ddev->mode_switch); + + mutex_unlock(&ddev->write_lock); +} + +static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay) +{ + schedule_delayed_work(&ddev->mode_work, delay); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_base(struct sdtx_device *ddev, + struct ssam_bas_base_info info) +{ + struct sdtx_base_info_event event; + + lockdep_assert_held(&ddev->write_lock); + + /* Prevent duplicate events. */ + if (ddev->state.base.state == info.state && + ddev->state.base.base_id == info.base_id) + return; + + ddev->state.base = info; + + event.e.length = sizeof(struct sdtx_base_info); + event.e.code = SDTX_EVENT_BASE_CONNECTION; + event.v.state = sdtx_translate_base_state(ddev, info.state); + event.v.base_id = SDTX_BASE_TYPE_SSH(info.base_id); + + sdtx_push_event(ddev, &event.e); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_mode(struct sdtx_device *ddev, u8 mode) +{ + struct sdtx_status_event event; + int tablet; + + /* + * Note: This function must be called after updating the base state + * via __sdtx_device_state_update_base(), as we rely on the updated + * base state value in the validity check below. + */ + + lockdep_assert_held(&ddev->write_lock); + + if (sdtx_device_mode_invalid(mode, ddev->state.base.state)) { + dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); + sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); + return; + } + + /* Prevent duplicate events. */ + if (ddev->state.device_mode == mode) + return; + + ddev->state.device_mode = mode; + + /* Send event. */ + event.e.length = sizeof(u16); + event.e.code = SDTX_EVENT_DEVICE_MODE; + event.v = mode; + + sdtx_push_event(ddev, &event.e); + + /* Send SW_TABLET_MODE event. */ + tablet = mode != SDTX_DEVICE_MODE_LAPTOP; + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); + input_sync(ddev->mode_switch); +} + +/* Must be executed with ddev->write_lock held. */ +static void __sdtx_device_state_update_latch(struct sdtx_device *ddev, u8 status) +{ + struct sdtx_status_event event; + + lockdep_assert_held(&ddev->write_lock); + + /* Prevent duplicate events. */ + if (ddev->state.latch_status == status) + return; + + ddev->state.latch_status = status; + + event.e.length = sizeof(struct sdtx_base_info); + event.e.code = SDTX_EVENT_BASE_CONNECTION; + event.v = sdtx_translate_latch_status(ddev, status); + + sdtx_push_event(ddev, &event.e); +} + +static void sdtx_device_state_workfn(struct work_struct *work) +{ + struct sdtx_device *ddev = container_of(work, struct sdtx_device, state_work.work); + struct ssam_bas_base_info base; + u8 mode, latch; + int status; + + /* Mark everything as dirty. */ + set_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); + set_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); + set_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); + + /* + * Ensure that the state gets marked as dirty before continuing to + * query it. Necessary to ensure that clear_bit() calls in + * sdtx_notifier() and sdtx_device_mode_workfn() actually clear these + * bits if an event is received while updating the state here. + */ + smp_mb__after_atomic(); + + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); + if (status) { + dev_err(ddev->dev, "failed to get base state: %d\n", status); + return; + } + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); + if (status) { + dev_err(ddev->dev, "failed to get device mode: %d\n", status); + return; + } + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); + if (status) { + dev_err(ddev->dev, "failed to get latch status: %d\n", status); + return; + } + + mutex_lock(&ddev->write_lock); + + /* + * If the respective dirty-bit has been cleared, an event has been + * received, updating this state. The queried state may thus be out of + * date. At this point, we can safely assume that the state provided + * by the event is either up to date, or we're about to receive + * another event updating it. + */ + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags)) + __sdtx_device_state_update_base(ddev, base); + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags)) + __sdtx_device_state_update_mode(ddev, mode); + + if (test_and_clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags)) + __sdtx_device_state_update_latch(ddev, latch); + + mutex_unlock(&ddev->write_lock); +} + +static void sdtx_update_device_state(struct sdtx_device *ddev, unsigned long delay) +{ + schedule_delayed_work(&ddev->state_work, delay); +} + + +/* -- Common device initialization. ----------------------------------------- */ + +static int sdtx_device_init(struct sdtx_device *ddev, struct device *dev, + struct ssam_controller *ctrl) +{ + int status, tablet_mode; + + /* Basic initialization. */ + kref_init(&ddev->kref); + init_rwsem(&ddev->lock); + ddev->dev = dev; + ddev->ctrl = ctrl; + + ddev->mdev.minor = MISC_DYNAMIC_MINOR; + ddev->mdev.name = "surface_dtx"; + ddev->mdev.nodename = "surface/dtx"; + ddev->mdev.fops = &surface_dtx_fops; + + ddev->notif.base.priority = 1; + ddev->notif.base.fn = sdtx_notifier; + ddev->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; + ddev->notif.event.id.target_category = SSAM_SSH_TC_BAS; + ddev->notif.event.id.instance = 0; + ddev->notif.event.mask = SSAM_EVENT_MASK_NONE; + ddev->notif.event.flags = SSAM_EVENT_SEQUENCED; + + init_waitqueue_head(&ddev->waitq); + mutex_init(&ddev->write_lock); + init_rwsem(&ddev->client_lock); + INIT_LIST_HEAD(&ddev->client_list); + + INIT_DELAYED_WORK(&ddev->mode_work, sdtx_device_mode_workfn); + INIT_DELAYED_WORK(&ddev->state_work, sdtx_device_state_workfn); + + /* + * Get current device state. We want to guarantee that events are only + * sent when state actually changes. Thus we cannot use special + * "uninitialized" values, as that would cause problems when manually + * querying the state in surface_dtx_pm_complete(). I.e. we would not + * be able to detect state changes there if no change event has been + * received between driver initialization and first device suspension. + * + * Note that we also need to do this before registering the event + * notifier, as that may access the state values. + */ + status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &ddev->state.base); + if (status) + return status; + + status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &ddev->state.device_mode); + if (status) + return status; + + status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &ddev->state.latch_status); + if (status) + return status; + + /* Set up tablet mode switch. */ + ddev->mode_switch = input_allocate_device(); + if (!ddev->mode_switch) + return -ENOMEM; + + ddev->mode_switch->name = "Microsoft Surface DTX Device Mode Switch"; + ddev->mode_switch->phys = "ssam/01:11:01:00:00/input0"; + ddev->mode_switch->id.bustype = BUS_HOST; + ddev->mode_switch->dev.parent = ddev->dev; + + tablet_mode = (ddev->state.device_mode != SDTX_DEVICE_MODE_LAPTOP); + input_set_capability(ddev->mode_switch, EV_SW, SW_TABLET_MODE); + input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet_mode); + + status = input_register_device(ddev->mode_switch); + if (status) { + input_free_device(ddev->mode_switch); + return status; + } + + /* Set up event notifier. */ + status = ssam_notifier_register(ddev->ctrl, &ddev->notif); + if (status) + goto err_notif; + + /* Register miscdevice. */ + status = misc_register(&ddev->mdev); + if (status) + goto err_mdev; + + /* + * Update device state in case it has changed between getting the + * initial mode and registering the event notifier. + */ + sdtx_update_device_state(ddev, 0); + return 0; + +err_notif: + ssam_notifier_unregister(ddev->ctrl, &ddev->notif); + cancel_delayed_work_sync(&ddev->mode_work); +err_mdev: + input_unregister_device(ddev->mode_switch); + return status; +} + +static struct sdtx_device *sdtx_device_create(struct device *dev, struct ssam_controller *ctrl) +{ + struct sdtx_device *ddev; + int status; + + ddev = kzalloc(sizeof(*ddev), GFP_KERNEL); + if (!ddev) + return ERR_PTR(-ENOMEM); + + status = sdtx_device_init(ddev, dev, ctrl); + if (status) { + sdtx_device_put(ddev); + return ERR_PTR(status); + } + + return ddev; +} + +static void sdtx_device_destroy(struct sdtx_device *ddev) +{ + struct sdtx_client *client; + + /* + * Mark device as shut-down. Prevent new clients from being added and + * new operations from being executed. + */ + set_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags); + + /* Disable notifiers, prevent new events from arriving. */ + ssam_notifier_unregister(ddev->ctrl, &ddev->notif); + + /* Stop mode_work, prevent access to mode_switch. */ + cancel_delayed_work_sync(&ddev->mode_work); + + /* Stop state_work. */ + cancel_delayed_work_sync(&ddev->state_work); + + /* With mode_work canceled, we can unregister the mode_switch. */ + input_unregister_device(ddev->mode_switch); + + /* Wake up async clients. */ + down_write(&ddev->client_lock); + list_for_each_entry(client, &ddev->client_list, node) { + kill_fasync(&client->fasync, SIGIO, POLL_HUP); + } + up_write(&ddev->client_lock); + + /* Wake up blocking clients. */ + wake_up_interruptible(&ddev->waitq); + + /* + * Wait for clients to finish their current operation. After this, the + * controller and device references are guaranteed to be no longer in + * use. + */ + down_write(&ddev->lock); + ddev->dev = NULL; + ddev->ctrl = NULL; + up_write(&ddev->lock); + + /* Finally remove the misc-device. */ + misc_deregister(&ddev->mdev); + + /* + * We're now guaranteed that sdtx_device_open() won't be called any + * more, so we can now drop out reference. + */ + sdtx_device_put(ddev); +} + + +/* -- PM ops. --------------------------------------------------------------- */ + +#ifdef CONFIG_PM_SLEEP + +static void surface_dtx_pm_complete(struct device *dev) +{ + struct sdtx_device *ddev = dev_get_drvdata(dev); + + /* + * Normally, the EC will store events while suspended (i.e. in + * display-off state) and release them when resumed (i.e. transitioned + * to display-on state). During hibernation, however, the EC will be + * shut down and does not store events. Furthermore, events might be + * dropped during prolonged suspension (it is currently unknown how + * big this event buffer is and how it behaves on overruns). + * + * To prevent any problems, we update the device state here. We do + * this delayed to ensure that any events sent by the EC directly + * after resuming will be handled first. The delay below has been + * chosen (experimentally), so that there should be ample time for + * these events to be handled, before we check and, if necessary, + * update the state. + */ + sdtx_update_device_state(ddev, msecs_to_jiffies(1000)); +} + +static const struct dev_pm_ops surface_dtx_pm_ops = { + .complete = surface_dtx_pm_complete, +}; + +#else /* CONFIG_PM_SLEEP */ + +static const struct dev_pm_ops surface_dtx_pm_ops = {}; + +#endif /* CONFIG_PM_SLEEP */ + + +/* -- Platform driver. ------------------------------------------------------ */ + +static int surface_dtx_platform_probe(struct platform_device *pdev) +{ + struct ssam_controller *ctrl; + struct sdtx_device *ddev; + + /* Link to EC. */ + ctrl = ssam_client_bind(&pdev->dev); + if (IS_ERR(ctrl)) + return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); + + ddev = sdtx_device_create(&pdev->dev, ctrl); + if (IS_ERR(ddev)) + return PTR_ERR(ddev); + + platform_set_drvdata(pdev, ddev); + return 0; +} + +static int surface_dtx_platform_remove(struct platform_device *pdev) +{ + sdtx_device_destroy(platform_get_drvdata(pdev)); + return 0; +} + +static const struct acpi_device_id surface_dtx_acpi_match[] = { + { "MSHW0133", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, surface_dtx_acpi_match); + +static struct platform_driver surface_dtx_platform_driver = { + .probe = surface_dtx_platform_probe, + .remove = surface_dtx_platform_remove, + .driver = { + .name = "surface_dtx_pltf", + .acpi_match_table = surface_dtx_acpi_match, + .pm = &surface_dtx_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + + +/* -- SSAM device driver. --------------------------------------------------- */ + +#ifdef CONFIG_SURFACE_AGGREGATOR_BUS + +static int surface_dtx_ssam_probe(struct ssam_device *sdev) +{ + struct sdtx_device *ddev; + + ddev = sdtx_device_create(&sdev->dev, sdev->ctrl); + if (IS_ERR(ddev)) + return PTR_ERR(ddev); + + ssam_device_set_drvdata(sdev, ddev); + return 0; +} + +static void surface_dtx_ssam_remove(struct ssam_device *sdev) +{ + sdtx_device_destroy(ssam_device_get_drvdata(sdev)); +} + +static const struct ssam_device_id surface_dtx_ssam_match[] = { + { SSAM_SDEV(BAS, 0x01, 0x00, 0x00) }, + { }, +}; +MODULE_DEVICE_TABLE(ssam, surface_dtx_ssam_match); + +static struct ssam_device_driver surface_dtx_ssam_driver = { + .probe = surface_dtx_ssam_probe, + .remove = surface_dtx_ssam_remove, + .match_table = surface_dtx_ssam_match, + .driver = { + .name = "surface_dtx", + .pm = &surface_dtx_pm_ops, + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; + +static int ssam_dtx_driver_register(void) +{ + return ssam_device_driver_register(&surface_dtx_ssam_driver); +} + +static void ssam_dtx_driver_unregister(void) +{ + ssam_device_driver_unregister(&surface_dtx_ssam_driver); +} + +#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ + +static int ssam_dtx_driver_register(void) +{ + return 0; +} + +static void ssam_dtx_driver_unregister(void) +{ +} + +#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ + + +/* -- Module setup. --------------------------------------------------------- */ + +static int __init surface_dtx_init(void) +{ + int status; + + status = ssam_dtx_driver_register(); + if (status) + return status; + + status = platform_driver_register(&surface_dtx_platform_driver); + if (status) + ssam_dtx_driver_unregister(); + + return status; +} +module_init(surface_dtx_init); + +static void __exit surface_dtx_exit(void) +{ + platform_driver_unregister(&surface_dtx_platform_driver); + ssam_dtx_driver_unregister(); +} +module_exit(surface_dtx_exit); + +MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); +MODULE_DESCRIPTION("Detachment-system driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surface_platform_profile.c b/drivers/platform/surface/surface_platform_profile.c new file mode 100644 index 000000000000..6373d3b5eb7f --- /dev/null +++ b/drivers/platform/surface/surface_platform_profile.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Platform Profile / Performance Mode driver for Surface System + * Aggregator Module (thermal subsystem). + * + * Copyright (C) 2021 Maximilian Luz <luzmaximilian@gmail.com> + */ + +#include <asm/unaligned.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_profile.h> +#include <linux/types.h> + +#include <linux/surface_aggregator/device.h> + +enum ssam_tmp_profile { + SSAM_TMP_PROFILE_NORMAL = 1, + SSAM_TMP_PROFILE_BATTERY_SAVER = 2, + SSAM_TMP_PROFILE_BETTER_PERFORMANCE = 3, + SSAM_TMP_PROFILE_BEST_PERFORMANCE = 4, +}; + +struct ssam_tmp_profile_info { + __le32 profile; + __le16 unknown1; + __le16 unknown2; +} __packed; + +struct ssam_tmp_profile_device { + struct ssam_device *sdev; + struct platform_profile_handler handler; +}; + +SSAM_DEFINE_SYNC_REQUEST_CL_R(__ssam_tmp_profile_get, struct ssam_tmp_profile_info, { + .target_category = SSAM_SSH_TC_TMP, + .command_id = 0x02, +}); + +SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_profile_set, __le32, { + .target_category = SSAM_SSH_TC_TMP, + .command_id = 0x03, +}); + +static int ssam_tmp_profile_get(struct ssam_device *sdev, enum ssam_tmp_profile *p) +{ + struct ssam_tmp_profile_info info; + int status; + + status = ssam_retry(__ssam_tmp_profile_get, sdev, &info); + if (status < 0) + return status; + + *p = le32_to_cpu(info.profile); + return 0; +} + +static int ssam_tmp_profile_set(struct ssam_device *sdev, enum ssam_tmp_profile p) +{ + __le32 profile_le = cpu_to_le32(p); + + return ssam_retry(__ssam_tmp_profile_set, sdev, &profile_le); +} + +static int convert_ssam_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p) +{ + switch (p) { + case SSAM_TMP_PROFILE_NORMAL: + return PLATFORM_PROFILE_BALANCED; + + case SSAM_TMP_PROFILE_BATTERY_SAVER: + return PLATFORM_PROFILE_LOW_POWER; + + case SSAM_TMP_PROFILE_BETTER_PERFORMANCE: + return PLATFORM_PROFILE_BALANCED_PERFORMANCE; + + case SSAM_TMP_PROFILE_BEST_PERFORMANCE: + return PLATFORM_PROFILE_PERFORMANCE; + + default: + dev_err(&sdev->dev, "invalid performance profile: %d", p); + return -EINVAL; + } +} + +static int convert_profile_to_ssam(struct ssam_device *sdev, enum platform_profile_option p) +{ + switch (p) { + case PLATFORM_PROFILE_LOW_POWER: + return SSAM_TMP_PROFILE_BATTERY_SAVER; + + case PLATFORM_PROFILE_BALANCED: + return SSAM_TMP_PROFILE_NORMAL; + + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + return SSAM_TMP_PROFILE_BETTER_PERFORMANCE; + + case PLATFORM_PROFILE_PERFORMANCE: + return SSAM_TMP_PROFILE_BEST_PERFORMANCE; + + default: + /* This should have already been caught by platform_profile_store(). */ + WARN(true, "unsupported platform profile"); + return -EOPNOTSUPP; + } +} + +static int ssam_platform_profile_get(struct platform_profile_handler *pprof, + enum platform_profile_option *profile) +{ + struct ssam_tmp_profile_device *tpd; + enum ssam_tmp_profile tp; + int status; + + tpd = container_of(pprof, struct ssam_tmp_profile_device, handler); + + status = ssam_tmp_profile_get(tpd->sdev, &tp); + if (status) + return status; + + status = convert_ssam_to_profile(tpd->sdev, tp); + if (status < 0) + return status; + + *profile = status; + return 0; +} + +static int ssam_platform_profile_set(struct platform_profile_handler *pprof, + enum platform_profile_option profile) +{ + struct ssam_tmp_profile_device *tpd; + int tp; + + tpd = container_of(pprof, struct ssam_tmp_profile_device, handler); + + tp = convert_profile_to_ssam(tpd->sdev, profile); + if (tp < 0) + return tp; + + return ssam_tmp_profile_set(tpd->sdev, tp); +} + +static int surface_platform_profile_probe(struct ssam_device *sdev) +{ + struct ssam_tmp_profile_device *tpd; + + tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL); + if (!tpd) + return -ENOMEM; + + tpd->sdev = sdev; + + tpd->handler.profile_get = ssam_platform_profile_get; + tpd->handler.profile_set = ssam_platform_profile_set; + + set_bit(PLATFORM_PROFILE_LOW_POWER, tpd->handler.choices); + set_bit(PLATFORM_PROFILE_BALANCED, tpd->handler.choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, tpd->handler.choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, tpd->handler.choices); + + platform_profile_register(&tpd->handler); + return 0; +} + +static void surface_platform_profile_remove(struct ssam_device *sdev) +{ + platform_profile_remove(); +} + +static const struct ssam_device_id ssam_platform_profile_match[] = { + { SSAM_SDEV(TMP, 0x01, 0x00, 0x01) }, + { }, +}; +MODULE_DEVICE_TABLE(ssam, ssam_platform_profile_match); + +static struct ssam_device_driver surface_platform_profile = { + .probe = surface_platform_profile_probe, + .remove = surface_platform_profile_remove, + .match_table = ssam_platform_profile_match, + .driver = { + .name = "surface_platform_profile", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, +}; +module_ssam_device_driver(surface_platform_profile); + +MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); +MODULE_DESCRIPTION("Platform Profile Support for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/surface/surfacepro3_button.c b/drivers/platform/surface/surfacepro3_button.c index d8afed5db94c..242fb690dcaf 100644 --- a/drivers/platform/surface/surfacepro3_button.c +++ b/drivers/platform/surface/surfacepro3_button.c @@ -40,8 +40,6 @@ static const guid_t MSHW0040_DSM_UUID = #define SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_DOWN 0xc2 #define SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_DOWN 0xc3 -ACPI_MODULE_NAME("surface pro 3 button"); - MODULE_AUTHOR("Chen Yu"); MODULE_DESCRIPTION("Surface Pro3 Button Driver"); MODULE_LICENSE("GPL v2"); |