diff options
Diffstat (limited to 'drivers/platform/surface/surface_dtx.c')
| -rw-r--r-- | drivers/platform/surface/surface_dtx.c | 1289 | 
1 files changed, 1289 insertions, 0 deletions
| 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"); | 
