| /**************************************************************************** |
| * |
| * Copyright (c) 2016 Samsung Electronics Co., Ltd. All rights reserved. |
| * |
| ****************************************************************************/ |
| |
| #include <linux/module.h> |
| #include <linux/version.h> |
| #include <linux/slab.h> |
| #include <linux/cdev.h> |
| #include <linux/device.h> |
| #include <linux/fs.h> |
| #include <asm/uaccess.h> |
| #include <linux/delay.h> |
| #include <linux/proc_fs.h> |
| #include <linux/wakelock.h> |
| |
| #include <scsc/scsc_logring.h> |
| #include <scsc/scsc_mx.h> |
| #include "scsc_mx_impl.h" |
| |
| #ifdef CONFIG_SCSC_CLK20MHZ_TEST |
| #include "mx140_clk_test.h" |
| #endif |
| |
| /* Note: define MX140_CLK_VERBOSE_CALLBACKS to get more callbacks when events occur. |
| * without this, the only callbacks are failure/success from request() |
| */ |
| static int auto_start; |
| module_param(auto_start, int, S_IRUGO | S_IWUSR); |
| MODULE_PARM_DESC(auto_start, "Start service automatically: Default 0: disabled, 1: Enabled"); |
| |
| static DEFINE_MUTEX(clk_lock); |
| static DEFINE_MUTEX(clk_work_lock); |
| |
| struct workqueue_struct *mx140_clk20mhz_wq; |
| struct work_struct mx140_clk20mhz_work; |
| |
| static int recovery; |
| static int recovery_pending_stop_close; |
| #define MX140_SERVICE_RECOVERY_TIMEOUT 20000 |
| |
| /* Static Singleton */ |
| static struct mx140_clk20mhz { |
| /* scsc_service_client has to be the first */ |
| struct scsc_service_client mx140_clk20mhz_service_client; |
| struct scsc_service *mx140_clk20mhz_service; |
| struct scsc_mx *mx; |
| |
| atomic_t clk_request; |
| atomic_t maxwell_is_present; |
| atomic_t mx140_clk20mhz_service_started; |
| atomic_t request_pending; |
| atomic_t mx140_clk20mhz_service_failed; |
| |
| void *data; |
| void (*mx140_clk20mhz_client_cb)(void *data, enum mx140_clk20mhz_status event); |
| |
| struct proc_dir_entry *procfs_ctrl_dir; |
| u32 procfs_ctrl_dir_num; |
| |
| struct wake_lock clk_wake_lock; |
| struct completion recovery_probe_completion; |
| |
| } clk20mhz; |
| |
| static void mx140_clk20mhz_wq_stop(void); |
| static int mx140_clk20mhz_stop_service(struct scsc_mx *mx); |
| |
| #ifndef AID_MXPROC |
| #define AID_MXPROC 0 |
| #endif |
| |
| static void mx140_clk20mhz_restart(void); |
| |
| #define MX_CLK20_DIRLEN 128 |
| static const char *procdir_ctrl = "driver/mx140_clk"; |
| static u32 proc_count; |
| |
| /* Framework for registering proc handlers */ |
| #define MX_CLK20_PROCFS_RW_FILE_OPS(name) \ |
| static ssize_t mx_procfs_ ## name ## _write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos); \ |
| static ssize_t mx_procfs_ ## name ## _read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos); \ |
| static const struct file_operations mx_procfs_ ## name ## _fops = { \ |
| .read = mx_procfs_ ## name ## _read, \ |
| .write = mx_procfs_ ## name ## _write, \ |
| .open = mx_clk20_procfs_generic_open, \ |
| .llseek = generic_file_llseek \ |
| } |
| #define MX_CLK20_PROCFS_RO_FILE_OPS(name) \ |
| static ssize_t mx_procfs_ ## name ## _read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos); \ |
| static const struct file_operations mx_procfs_ ## name ## _fops = { \ |
| .read = mx_procfs_ ## name ## _read, \ |
| .open = mx_clk20_procfs_generic_open, \ |
| .llseek = generic_file_llseek \ |
| } |
| |
| #define MX_PDE_DATA(inode) PDE_DATA(inode) |
| |
| #define MX_CLK20_PROCFS_SET_UID_GID(_entry) \ |
| do { \ |
| kuid_t proc_kuid = KUIDT_INIT(AID_MXPROC); \ |
| kgid_t proc_kgid = KGIDT_INIT(AID_MXPROC); \ |
| proc_set_user(_entry, proc_kuid, proc_kgid); \ |
| } while (0) |
| |
| #define MX_CLK20_PROCFS_ADD_FILE(_sdev, name, parent, mode) \ |
| do { \ |
| struct proc_dir_entry *entry = proc_create_data(# name, mode, parent, &mx_procfs_ ## name ## _fops, _sdev); \ |
| MX_CLK20_PROCFS_SET_UID_GID(entry); \ |
| } while (0) |
| |
| #define MX_CLK20_PROCFS_REMOVE_FILE(name, parent) remove_proc_entry(# name, parent) |
| |
| /* Open handler */ |
| static int mx_clk20_procfs_generic_open(struct inode *inode, struct file *file) |
| { |
| file->private_data = MX_PDE_DATA(inode); |
| return 0; |
| } |
| |
| /* No-op */ |
| static ssize_t mx_procfs_restart_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) |
| { |
| (void)user_buf; |
| (void)count; |
| (void)ppos; |
| |
| return 0; |
| } |
| |
| /* Restart clock service */ |
| static ssize_t mx_procfs_restart_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) |
| { |
| (void)file; |
| (void)user_buf; |
| (void)ppos; |
| |
| mx140_clk20mhz_restart(); |
| |
| SCSC_TAG_INFO(MX_PROC, "OK\n"); |
| return count; |
| } |
| |
| /* Register proc handler */ |
| MX_CLK20_PROCFS_RW_FILE_OPS(restart); |
| |
| /* Populate proc node */ |
| static int mx140_clk20mhz_create_ctrl_proc_dir(struct mx140_clk20mhz *clk20mhz) |
| { |
| char dir[MX_CLK20_DIRLEN]; |
| struct proc_dir_entry *parent; |
| |
| (void)snprintf(dir, sizeof(dir), "%s%d", procdir_ctrl, proc_count); |
| parent = proc_mkdir(dir, NULL); |
| if (!parent) { |
| SCSC_TAG_ERR(MX_PROC, "failed to create proc dir %s\n", procdir_ctrl); |
| return -EINVAL; |
| } |
| #if (LINUX_VERSION_CODE <= KERNEL_VERSION(3, 4, 0)) |
| parent->data = clk20mhz; |
| #endif |
| clk20mhz->procfs_ctrl_dir = parent; |
| clk20mhz->procfs_ctrl_dir_num = proc_count; |
| MX_CLK20_PROCFS_ADD_FILE(clk20mhz, restart, parent, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); |
| SCSC_TAG_DEBUG(MX_PROC, "created %s proc dir\n", dir); |
| proc_count++; |
| |
| return 0; |
| } |
| |
| /* Remove proc node */ |
| static void mx140_clk20mhz_remove_ctrl_proc_dir(struct mx140_clk20mhz *clk20mhz) |
| { |
| if (clk20mhz->procfs_ctrl_dir) { |
| char dir[MX_CLK20_DIRLEN]; |
| |
| MX_CLK20_PROCFS_REMOVE_FILE(restart, clk20mhz->procfs_ctrl_dir); |
| (void)snprintf(dir, sizeof(dir), "%s%d", procdir_ctrl, clk20mhz->procfs_ctrl_dir_num); |
| remove_proc_entry(dir, NULL); |
| clk20mhz->procfs_ctrl_dir = NULL; |
| proc_count--; |
| SCSC_TAG_DEBUG(MX_PROC, "removed %s proc dir\n", dir); |
| } |
| } |
| |
| /* Maxwell manager has detected a recoverable issue no action needed */ |
| static u8 mx140_clk20mhz_failure_notification(struct scsc_service_client *client, struct mx_syserr_decode *err) |
| { |
| (void) client; |
| SCSC_TAG_INFO(CLK20, "\n"); |
| return err->level; |
| } |
| |
| /* Maxwell manager has detected an issue and the service should freeze */ |
| static bool mx140_clk20mhz_stop_on_failure(struct scsc_service_client *client, struct mx_syserr_decode *err) |
| { |
| (void) err; |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_failed, 1); |
| |
| mutex_lock(&clk_work_lock); |
| recovery = 1; |
| reinit_completion(&clk20mhz.recovery_probe_completion); |
| mutex_unlock(&clk_work_lock); |
| |
| #ifdef MX140_CLK_VERBOSE_CALLBACKS |
| /* If call back is registered, inform the user about an asynchronous failure */ |
| if (clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_ASYNC_FAIL); |
| #endif |
| |
| SCSC_TAG_INFO(CLK20, "\n"); |
| |
| return false; |
| } |
| |
| /* Maxwell manager has handled a failure and the chip has been resat. */ |
| static void mx140_clk20mhz_failure_reset(struct scsc_service_client *client, u8 level, u16 scsc_syserr_code) |
| { |
| (void)level; |
| (void)scsc_syserr_code; |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_failed, 1); |
| |
| #ifdef MX140_CLK_VERBOSE_CALLBACKS |
| /* If call back is registered, inform the user about an asynchronous failure */ |
| if (clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_ASYNC_FAIL); |
| #endif |
| SCSC_TAG_INFO(CLK20, "\n"); |
| } |
| |
| static int mx140_clk20mhz_start_service(struct scsc_mx *mx) |
| { |
| int r; |
| |
| /* Open the service and get resource pointers */ |
| clk20mhz.mx140_clk20mhz_service = scsc_mx_service_open(mx, SCSC_SERVICE_ID_CLK20MHZ, &clk20mhz.mx140_clk20mhz_service_client, &r); |
| if (!clk20mhz.mx140_clk20mhz_service) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_open failed %d\n", r); |
| return r; |
| } |
| |
| /* In case of recovery ensure WLBT has ownership */ |
| if (atomic_read(&clk20mhz.mx140_clk20mhz_service_failed)) { |
| struct scsc_mif_abs *mif = scsc_mx_get_mif_abs(clk20mhz.mx); |
| |
| if (!mif) |
| goto error; |
| |
| if (mif->mif_restart) |
| mif->mif_restart(mif); |
| |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_failed, 0); |
| } |
| |
| /* Start service. Will bring-up the chip if the chip is disabled */ |
| if (scsc_mx_service_start(clk20mhz.mx140_clk20mhz_service, 0)) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_start failed\n"); |
| goto error; |
| } |
| |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 1); |
| |
| /* If call back is registered, inform the user that the service has started */ |
| if (atomic_read(&clk20mhz.clk_request) && clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_STARTED); |
| |
| return 0; |
| error: |
| return -EIO; |
| } |
| |
| static int mx140_clk20mhz_stop_service(struct scsc_mx *mx) |
| { |
| int r; |
| |
| if (!atomic_read(&clk20mhz.mx140_clk20mhz_service_started)) { |
| SCSC_TAG_INFO(CLK20, "Service not started\n"); |
| return -ENODEV; |
| } |
| |
| /* Stop service. */ |
| if (scsc_mx_service_stop(clk20mhz.mx140_clk20mhz_service)) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_stop failed\n"); |
| #ifdef MX140_CLK_VERBOSE_CALLBACKS |
| /* If call back is registered, inform the user that the service has failed to stop */ |
| if (clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_NOT_STOPPED); |
| return -EIO; |
| #endif |
| } |
| |
| /* Ignore a service_stop timeout above as it's better to try to close */ |
| |
| /* Close service, if no other service is using Maxwell, chip will turn off */ |
| r = scsc_mx_service_close(clk20mhz.mx140_clk20mhz_service); |
| if (r) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_close failed\n"); |
| |
| #ifdef MX140_CLK_VERBOSE_CALLBACKS |
| /* If call back is registered, inform the user that the service has failed to close */ |
| if (clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_NOT_STOPPED); |
| return -EIO; |
| #endif |
| } |
| |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| |
| #ifdef MX140_CLK_VERBOSE_CALLBACKS |
| /* If call back is registered, inform the user that the service has stopped */ |
| if (clk20mhz.mx140_clk20mhz_client_cb) |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, MX140_CLK_STOPPED); |
| #endif |
| return 0; |
| } |
| |
| #define MX140_CLK_TRIES (20) |
| |
| static void mx140_clk20mhz_work_func(struct work_struct *work) |
| { |
| int i; |
| int r = 0; |
| enum mx140_clk20mhz_status status; |
| |
| mutex_lock(&clk_work_lock); |
| |
| for (i = 0; i < MX140_CLK_TRIES; i++) { |
| if (atomic_read(&clk20mhz.clk_request) == 0) { |
| SCSC_TAG_INFO(CLK20, "mx140_clk20mhz_start_service no longer requested\n"); |
| recovery = 0; |
| mutex_unlock(&clk_work_lock); |
| return; |
| } |
| |
| SCSC_TAG_INFO(CLK20, "Calling mx140_clk20mhz_start_service\n"); |
| r = mx140_clk20mhz_start_service(clk20mhz.mx); |
| switch (r) { |
| case 0: |
| SCSC_TAG_INFO(CLK20, "mx140_clk20mhz_start_service OK\n"); |
| recovery = 0; |
| mutex_unlock(&clk_work_lock); |
| return; |
| case -EAGAIN: |
| SCSC_TAG_INFO(CLK20, "FW not found because filesystem not mounted yet, retrying...\n"); |
| msleep(500); /* No FS yet, retry */ |
| break; |
| default: |
| SCSC_TAG_INFO(CLK20, "mx140_clk20mhz_start_service failed %d\n", r); |
| goto err; |
| } |
| } |
| err: |
| SCSC_TAG_ERR(CLK20, "Unable to start the 20MHz clock service\n"); |
| |
| /* Deferred service start failure or timeout. |
| * We assume it'll never manage to start - e.g. bad or missing f/w. |
| */ |
| if (r) { |
| struct scsc_mif_abs *mif = scsc_mx_get_mif_abs(clk20mhz.mx); |
| |
| SCSC_TAG_ERR(CLK20, "Deferred start timeout (%d)\n", r); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_failed, 1); |
| |
| /* Switch USBPLL ownership to AP so USB may be used */ |
| if (mif && mif->mif_cleanup) |
| mif->mif_cleanup(mif); |
| } |
| |
| /* If call back is registered, inform the user that the service has failed to start */ |
| if (atomic_read(&clk20mhz.clk_request) && clk20mhz.mx140_clk20mhz_client_cb) { |
| /* The USB/PLL driver has inadequate error handing... |
| * Lie that the start was successful when AP has control |
| */ |
| status = atomic_read(&clk20mhz.mx140_clk20mhz_service_failed) ? MX140_CLK_STARTED : MX140_CLK_NOT_STARTED; |
| |
| /* Also lie that the start was successful when the mx140 driver is halted after f/w panic */ |
| if (r == -EILSEQ || r == -EPERM) |
| status = MX140_CLK_STARTED; |
| |
| SCSC_TAG_INFO(CLK20, "cb %d\n", status); |
| clk20mhz.mx140_clk20mhz_client_cb(clk20mhz.data, status); |
| } |
| recovery = 0; |
| mutex_unlock(&clk_work_lock); |
| } |
| |
| static void mx140_clk20mhz_wq_init(void) |
| { |
| mx140_clk20mhz_wq = create_singlethread_workqueue("mx140_clk20mhz_wq"); |
| INIT_WORK(&mx140_clk20mhz_work, mx140_clk20mhz_work_func); |
| } |
| |
| static void mx140_clk20mhz_wq_stop(void) |
| { |
| cancel_work_sync(&mx140_clk20mhz_work); |
| flush_workqueue(mx140_clk20mhz_wq); |
| } |
| |
| static void mx140_clk20mhz_wq_deinit(void) |
| { |
| mx140_clk20mhz_wq_stop(); |
| destroy_workqueue(mx140_clk20mhz_wq); |
| } |
| |
| static void mx140_clk20mhz_wq_start(void) |
| { |
| queue_work(mx140_clk20mhz_wq, &mx140_clk20mhz_work); |
| } |
| |
| /* Register a callback function to indicate to the (USB) client the status of |
| * the clock request |
| */ |
| int mx140_clk20mhz_register(void (*client_cb)(void *data, enum mx140_clk20mhz_status event), void *data) |
| { |
| SCSC_TAG_INFO(CLK20, "cb %p, %p\n", client_cb, data); |
| |
| mutex_lock(&clk_lock); |
| if (clk20mhz.mx140_clk20mhz_client_cb == NULL) { |
| SCSC_TAG_INFO(CLK20, "clk20Mhz client registered\n"); |
| clk20mhz.mx140_clk20mhz_client_cb = client_cb; |
| clk20mhz.data = data; |
| mutex_unlock(&clk_lock); |
| return 0; |
| } |
| |
| SCSC_TAG_ERR(CLK20, "clk20Mhz client already registered\n"); |
| mutex_unlock(&clk_lock); |
| return -EEXIST; |
| } |
| EXPORT_SYMBOL(mx140_clk20mhz_register); |
| |
| /* Unregister callback function */ |
| void mx140_clk20mhz_unregister(void) |
| { |
| SCSC_TAG_INFO(CLK20, "\n"); |
| |
| mutex_lock(&clk_lock); |
| if (clk20mhz.mx140_clk20mhz_client_cb == NULL) { |
| SCSC_TAG_INFO(CLK20, "clk20Mhz client not registered\n"); |
| mutex_unlock(&clk_lock); |
| return; |
| } |
| |
| clk20mhz.mx140_clk20mhz_client_cb = NULL; |
| clk20mhz.data = NULL; |
| mutex_unlock(&clk_lock); |
| } |
| EXPORT_SYMBOL(mx140_clk20mhz_unregister); |
| |
| /* Indicate that an external client requires mx140's 20 MHz clock. |
| * The Core driver will boot mx140 as required and ensure that the |
| * clock remains running. |
| * |
| * If a callback was installed by register(), do this asynchronously. |
| */ |
| int mx140_clk20mhz_request(void) |
| { |
| mutex_lock(&clk_lock); |
| atomic_inc(&clk20mhz.clk_request); |
| |
| SCSC_TAG_INFO(CLK20, "%d\n", atomic_read(&clk20mhz.clk_request)); |
| |
| if (!atomic_read(&clk20mhz.maxwell_is_present)) { |
| SCSC_TAG_INFO(CLK20, "Maxwell is not present yet, store request\n"); |
| atomic_set(&clk20mhz.request_pending, 1); |
| mutex_unlock(&clk_lock); |
| return 0; |
| } |
| |
| if (recovery) { |
| int r; |
| |
| mutex_unlock(&clk_lock); |
| r = wait_for_completion_timeout(&clk20mhz.recovery_probe_completion, |
| msecs_to_jiffies(MX140_SERVICE_RECOVERY_TIMEOUT)); |
| mutex_lock(&clk_lock); |
| if (r == 0) { |
| SCSC_TAG_INFO(CLK20, "recovery_probe_completion timeout - try a start\n"); |
| mx140_clk20mhz_wq_start(); |
| } |
| } else if (!atomic_read(&clk20mhz.mx140_clk20mhz_service_started)) |
| mx140_clk20mhz_wq_start(); |
| else |
| SCSC_TAG_INFO(CLK20, "Service already started\n"); |
| |
| mutex_unlock(&clk_lock); |
| return 0; |
| } |
| EXPORT_SYMBOL(mx140_clk20mhz_request); |
| |
| /* Indicate that an external client no requires mx140's 20 MHz clock |
| * The Core driver will shut down mx140 if no other services are |
| * currently running |
| * |
| * If a callback was installed by register(), do this asynchronously. |
| */ |
| int mx140_clk20mhz_release(void) |
| { |
| int ret = 0; |
| |
| mutex_lock(&clk_lock); |
| atomic_dec(&clk20mhz.clk_request); |
| SCSC_TAG_INFO(CLK20, "%d\n", atomic_read(&clk20mhz.clk_request)); |
| |
| if (!atomic_read(&clk20mhz.maxwell_is_present)) { |
| SCSC_TAG_INFO(CLK20, "Maxwell is released before probe\n"); |
| if (!atomic_read(&clk20mhz.request_pending)) { |
| SCSC_TAG_INFO(CLK20, "Maxwell had request pending. Cancel it\n"); |
| atomic_set(&clk20mhz.request_pending, 0); |
| } |
| mutex_unlock(&clk_lock); |
| return 0; |
| } |
| |
| /* Cancel any pending attempt */ |
| mx140_clk20mhz_wq_stop(); |
| |
| if (recovery) { |
| recovery_pending_stop_close = 1; |
| } else { |
| ret = mx140_clk20mhz_stop_service(clk20mhz.mx); |
| if (ret == -ENODEV) { |
| /* Suppress error if it wasn't running */ |
| ret = 0; |
| } |
| } |
| |
| /* Suppress stop failure if the service is failed */ |
| if (atomic_read(&clk20mhz.mx140_clk20mhz_service_failed)) { |
| SCSC_TAG_INFO(CLK20, "Return OK as control is with AP\n"); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| ret = 0; |
| } |
| |
| mutex_unlock(&clk_lock); |
| return ret; |
| } |
| EXPORT_SYMBOL(mx140_clk20mhz_release); |
| |
| /* Probe callback after platform driver is registered */ |
| void mx140_clk20mhz_probe(struct scsc_mx_module_client *module_client, struct scsc_mx *mx, enum scsc_module_client_reason reason) |
| { |
| SCSC_TAG_INFO(CLK20, "\n"); |
| |
| if (reason == SCSC_MODULE_CLIENT_REASON_RECOVERY && !recovery) { |
| SCSC_TAG_INFO(CLK20, "Ignore probe - no recovery in progress\n"); |
| return; |
| } |
| |
| if (reason == SCSC_MODULE_CLIENT_REASON_RECOVERY && recovery) { |
| SCSC_TAG_INFO(CLK20, "Recovery probe\n"); |
| |
| /** |
| * If recovery_pending_stop_close is set, then there was a stop |
| * during recovery (could be due to USB cable unplugged) so |
| * recovery should just stop here. |
| * The mx140_clk service has been closed in the remove callback. |
| */ |
| mutex_lock(&clk_lock); |
| if (recovery_pending_stop_close) { |
| SCSC_TAG_INFO(CLK20, "Recovery probe - stop during recovery, so don't recover\n"); |
| recovery_pending_stop_close = 0; |
| recovery = 0; |
| mutex_unlock(&clk_lock); |
| /** |
| * Should there have been a new start request during |
| * recovery (very unlikely), then the complete timeout |
| * will ensure that a start is requested. |
| */ |
| return; |
| } |
| mutex_unlock(&clk_lock); |
| |
| mutex_lock(&clk_work_lock); |
| mx140_clk20mhz_wq_start(); |
| mutex_unlock(&clk_work_lock); |
| complete_all(&clk20mhz.recovery_probe_completion); |
| } else { |
| SCSC_TAG_INFO(CLK20, "Maxwell probed\n"); |
| clk20mhz.mx = mx; |
| clk20mhz.mx140_clk20mhz_service_client.failure_notification = mx140_clk20mhz_failure_notification; |
| clk20mhz.mx140_clk20mhz_service_client.stop_on_failure_v2 = mx140_clk20mhz_stop_on_failure; |
| clk20mhz.mx140_clk20mhz_service_client.failure_reset_v2 = mx140_clk20mhz_failure_reset; |
| |
| mx140_clk20mhz_create_ctrl_proc_dir(&clk20mhz); |
| |
| mx140_clk20mhz_wq_init(); |
| |
| atomic_set(&clk20mhz.maxwell_is_present, 1); |
| |
| mutex_lock(&clk_work_lock); |
| if ((auto_start || atomic_read(&clk20mhz.request_pending))) { |
| atomic_set(&clk20mhz.request_pending, 0); |
| SCSC_TAG_INFO(CLK20, "start pending service\n"); |
| mx140_clk20mhz_wq_start(); |
| } |
| mutex_unlock(&clk_work_lock); |
| } |
| } |
| |
| |
| static void mx140_clk20mhz_restart(void) |
| { |
| int r; |
| struct scsc_mif_abs *mif; |
| |
| SCSC_TAG_INFO(CLK20, "\n"); |
| |
| wake_lock(&clk20mhz.clk_wake_lock); |
| |
| mutex_lock(&clk_lock); |
| |
| if (!atomic_read(&clk20mhz.mx140_clk20mhz_service_started)) { |
| SCSC_TAG_INFO(CLK20, "service wasn't started\n"); |
| goto done; |
| } |
| |
| mif = scsc_mx_get_mif_abs(clk20mhz.mx); |
| if (mif == NULL) |
| goto done; |
| |
| /* Don't stop the 20 MHz clock service. Leave it running until |
| * WLBT resets due to the service_close(). |
| */ |
| |
| /* Ensure USBPLL is running and owned by AP, to stop USB disconnect */ |
| if (mif->mif_cleanup) |
| mif->mif_cleanup(mif); |
| |
| r = scsc_mx_service_close(clk20mhz.mx140_clk20mhz_service); |
| if (r) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_close failed (%d)\n", r); |
| goto done; |
| } |
| |
| /* ...and restart the 20 MHz clock service */ |
| clk20mhz.mx140_clk20mhz_service = scsc_mx_service_open(clk20mhz.mx, SCSC_SERVICE_ID_CLK20MHZ, &clk20mhz.mx140_clk20mhz_service_client, &r); |
| if (clk20mhz.mx140_clk20mhz_service == NULL) { |
| SCSC_TAG_ERR(CLK20, "reopen failed %d\n", r); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| goto done; |
| } |
| |
| /* Ensure USBPLL is owned by WLBT again */ |
| if (mif->mif_restart) |
| mif->mif_restart(mif); |
| |
| r = scsc_mx_service_start(clk20mhz.mx140_clk20mhz_service, 0); |
| if (r) { |
| SCSC_TAG_ERR(CLK20, "restart failed %d\n", r); |
| r = scsc_mx_service_close(clk20mhz.mx140_clk20mhz_service); |
| if (r) |
| SCSC_TAG_ERR(CLK20, "scsc_mx_service_close failed %d\n", r); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| goto done; |
| } |
| |
| SCSC_TAG_INFO(CLK20, "restarted\n"); |
| done: |
| mutex_unlock(&clk_lock); |
| wake_unlock(&clk20mhz.clk_wake_lock); |
| } |
| |
| /* Remove callback platform driver is unregistered */ |
| void mx140_clk20mhz_remove(struct scsc_mx_module_client *module_client, struct scsc_mx *mx, enum scsc_module_client_reason reason) |
| { |
| int r; |
| |
| mutex_lock(&clk_work_lock); |
| if (reason == SCSC_MODULE_CLIENT_REASON_RECOVERY && !recovery) { |
| SCSC_TAG_INFO(CLK20, "Ignore recovery remove: Service driver not active\n"); |
| goto done; |
| } else if (reason == SCSC_MODULE_CLIENT_REASON_RECOVERY && recovery) { |
| struct scsc_mif_abs *mif; |
| |
| SCSC_TAG_INFO(CLK20, "Recovery remove\n"); |
| |
| mutex_lock(&clk_lock); |
| mx140_clk20mhz_wq_stop(); |
| |
| mif = scsc_mx_get_mif_abs(clk20mhz.mx); |
| if (mif == NULL) |
| goto done_local; |
| |
| /** |
| * If there's been a stop during recovery ensure that the |
| * mx140_clk service is closed in the mx driver, but do not |
| * touch USBPLL ownership since this will already have been |
| * handled. |
| */ |
| if (!recovery_pending_stop_close) { |
| /* Don't stop the clock service - leave it running until |
| * service_close() resets WLBT. |
| */ |
| |
| /* Switch ownership of USBPLL to the AP. Ownership |
| * returns to WLBT after recovery completes. |
| */ |
| if (mif->mif_cleanup) |
| mif->mif_cleanup(mif); |
| } |
| |
| r = scsc_mx_service_close(clk20mhz.mx140_clk20mhz_service); |
| if (r) |
| SCSC_TAG_INFO(CLK20, "scsc_mx_service_close failed %d\n", r); |
| |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| done_local: |
| mutex_unlock(&clk_lock); |
| } else { |
| SCSC_TAG_INFO(CLK20, "Maxwell removed\n"); |
| mx140_clk20mhz_remove_ctrl_proc_dir(&clk20mhz); |
| atomic_set(&clk20mhz.maxwell_is_present, 0); |
| mx140_clk20mhz_wq_deinit(); |
| } |
| |
| done: |
| mutex_unlock(&clk_work_lock); |
| } |
| |
| /* 20MHz client driver */ |
| struct scsc_mx_module_client mx140_clk20mhz_driver = { |
| .name = "MX 20MHz clock client", |
| .probe = mx140_clk20mhz_probe, |
| .remove = mx140_clk20mhz_remove, |
| }; |
| |
| /* 20MHz service driver initialization */ |
| static int __init mx140_clk20mhz_init(void) |
| { |
| int ret; |
| |
| SCSC_TAG_INFO(CLK20, "Registering service\n"); |
| |
| wake_lock_init(&clk20mhz.clk_wake_lock, WAKE_LOCK_SUSPEND, "clk20_wl"); |
| init_completion(&clk20mhz.recovery_probe_completion); |
| |
| atomic_set(&clk20mhz.clk_request, 0); |
| atomic_set(&clk20mhz.maxwell_is_present, 0); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_started, 0); |
| atomic_set(&clk20mhz.request_pending, 0); |
| atomic_set(&clk20mhz.mx140_clk20mhz_service_failed, 0); |
| |
| /* Register with Maxwell Framework */ |
| ret = scsc_mx_module_register_client_module(&mx140_clk20mhz_driver); |
| if (ret) { |
| SCSC_TAG_ERR(CLK20, "scsc_mx_module_register_client_module failed: r=%d\n", ret); |
| return ret; |
| } |
| |
| #ifdef CONFIG_SCSC_CLK20MHZ_TEST |
| mx140_clk_test_init(); |
| #endif |
| return 0; |
| } |
| |
| static void __exit mx140_clk20mhz_exit(void) |
| { |
| scsc_mx_module_unregister_client_module(&mx140_clk20mhz_driver); |
| #ifdef CONFIG_SCSC_CLK20MHZ_TEST |
| mx140_clk_test_exit(); |
| #endif |
| |
| complete_all(&clk20mhz.recovery_probe_completion); |
| wake_lock_destroy(&clk20mhz.clk_wake_lock); |
| } |
| |
| module_init(mx140_clk20mhz_init); |
| module_exit(mx140_clk20mhz_exit); |
| |
| MODULE_DESCRIPTION("Samsung Maxwell 20MHz Clock Service"); |
| MODULE_AUTHOR("SLSI"); |
| MODULE_LICENSE("GPL and additional rights"); |