| // 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-2022 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 = SSAM_SSH_TID_SAM, | |
| .command_id = 0x06, | |
| .instance_id = 0x00, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_unlock, { | |
| .target_category = SSAM_SSH_TC_BAS, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .command_id = 0x07, | |
| .instance_id = 0x00, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_request, { | |
| .target_category = SSAM_SSH_TC_BAS, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .command_id = 0x08, | |
| .instance_id = 0x00, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_confirm, { | |
| .target_category = SSAM_SSH_TC_BAS, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .command_id = 0x09, | |
| .instance_id = 0x00, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_heartbeat, { | |
| .target_category = SSAM_SSH_TC_BAS, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .command_id = 0x0a, | |
| .instance_id = 0x00, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_cancel, { | |
| .target_category = SSAM_SSH_TC_BAS, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .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 = SSAM_SSH_TID_SAM, | |
| .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 = SSAM_SSH_TID_SAM, | |
| .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 = SSAM_SSH_TID_SAM, | |
| .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_obj(*client); | |
| 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); | |
| mutex_destroy(&client->read_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 (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) | |
| return EPOLLHUP | EPOLLERR; | |
| poll_wait(file, &client->ddev->waitq, pt); | |
| if (!kfifo_is_empty(&client->buffer)) | |
| events |= EPOLLIN | EPOLLRDNORM; | |
| 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, | |
| }; | |
| /* -- 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_obj(*ddev); | |
| 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 void surface_dtx_platform_remove(struct platform_device *pdev) | |
| { | |
| sdtx_device_destroy(platform_get_drvdata(pdev)); | |
| } | |
| 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, SAM, 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"); |