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"); | 
