mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1284 lines
33 KiB
1284 lines
33 KiB
// 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 <[email protected]> |
|
*/ |
|
|
|
#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); |
|
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, |
|
.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 <[email protected]>"); |
|
MODULE_DESCRIPTION("Detachment-system driver for Surface System Aggregator Module"); |
|
MODULE_LICENSE("GPL");
|
|
|