| /* drivers/switch/hds_fsa8008.c |
| * |
| * LGE 3.5 PI Headset detection driver using fsa8008. |
| * |
| * Copyright (C) 2008 Google, Inc. |
| * Author: Mike Lockwood <lockwood@android.com> |
| * |
| * Copyright (C) 2009-2012 LGE, Inc. |
| * Lee SungYoung <lsy@lge.com> |
| * Kim Eun Hye <ehgrace.kim@lge.com> |
| * Yoon Gi Souk <gisouk.yoon@lge.com> |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| /* Interface is following: |
| * android:frameworks/base/services/java/com/android/server/HeadsetObserver.java |
| * HEADSET_UEVENT_MATCH = "DEVPATH=/sys/devices/virtual/switch/h2w" |
| * HEADSET_STATE_PATH = /sys/class/switch/h2w/state |
| * HEADSET_NAME_PATH = /sys/class/switch/h2w/name |
| */ |
| #include <linux/module.h> |
| #include <linux/kernel.h> |
| #include <linux/init.h> |
| #include <linux/interrupt.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| #include <linux/switch.h> |
| #include <linux/workqueue.h> |
| #include <linux/delay.h> |
| #include <linux/gpio.h> |
| #include <linux/irq.h> |
| #include <linux/mutex.h> |
| #include <linux/hrtimer.h> |
| #include <linux/input.h> |
| #include <linux/debugfs.h> |
| #include <linux/wakelock.h> |
| #include <linux/platform_data/hds_fsa8008.h> |
| |
| #define FSA8008_USE_WORK_QUEUE |
| #define FSA8008_KEY_LATENCY_TIME 200 /* in ms */ |
| #define FSA8008_DEBOUNCE_TIME 500 /* in ms */ |
| #define FSA8008_WAKELOCK_TIMEOUT (2*HZ) |
| |
| #define HSD_DEBUG_PRINT |
| |
| #ifdef HSD_DEBUG_PRINT |
| #define HSD_DBG(fmt, args...) printk(KERN_DEBUG "%s: " fmt, __func__, ##args) |
| #else |
| #define HSD_DBG(fmt, args...) do {} while (0) |
| #endif |
| |
| #ifdef FSA8008_USE_WORK_QUEUE |
| static struct workqueue_struct *local_fsa8008_workqueue; |
| #endif |
| |
| static struct wake_lock ear_hook_wake_lock; |
| |
| struct hsd_info { |
| /* function devices provided by this driver */ |
| struct switch_dev sdev; |
| struct input_dev *input; |
| /* mutex */ |
| struct mutex mutex_lock; |
| /* h/w configuration : initilized by platform data */ |
| unsigned int gpio_detect; /* DET : to detect jack inserted or not */ |
| unsigned int gpio_detect_can_wakeup; |
| unsigned int gpio_mic_en; /* EN : to enable mic */ |
| unsigned int gpio_mic_bias_en; /* EN : to enable mic bias */ |
| unsigned int gpio_jpole; /* JPOLE : 3pole or 4pole */ |
| unsigned int gpio_key; /* S/E button */ |
| |
| /* callback function which is initialized while probing */ |
| void (*set_headset_mic_bias)(int enable); |
| void (*set_uart_console)(int enable); |
| |
| unsigned int latency_for_detection; |
| unsigned int latency_for_key; |
| |
| unsigned int key_code; |
| |
| /* irqs */ |
| unsigned int irq_detect; |
| unsigned int irq_key; |
| /* internal states */ |
| atomic_t irq_key_enabled; |
| atomic_t is_3_pole_or_not; |
| atomic_t btn_state; |
| int saved_detect; |
| /* work for detect_work */ |
| struct delayed_work work; |
| struct delayed_work work_for_key_pressed; |
| struct delayed_work work_for_key_released; |
| }; |
| |
| enum { |
| NO_DEVICE = 0, |
| HEADSET_WITH_MIC = (1 << 0), |
| HEADSET_NO_MIC = (1 << 1), |
| }; |
| enum { |
| HEADSET_INSERT = 0, |
| HEADSET_REMOVE = 1, |
| }; |
| |
| enum { |
| HEADSET_4POLE = 0, |
| HEADSET_3POLE = 1, |
| }; |
| |
| static ssize_t hsd_print_name(struct switch_dev *sdev, char *buf) |
| { |
| switch (switch_get_state(sdev)) { |
| case NO_DEVICE: |
| return sprintf(buf, "No Device\n"); |
| case HEADSET_WITH_MIC: |
| return sprintf(buf, "Headset\n"); |
| case HEADSET_NO_MIC: |
| return sprintf(buf, "Headset_no_mic\n"); |
| |
| } |
| return -EINVAL; |
| } |
| |
| static ssize_t hsd_print_state(struct switch_dev *sdev, char *buf) |
| { |
| return sprintf(buf, "%d\n", switch_get_state(sdev)); |
| } |
| |
| static void button_pressed(struct work_struct *work) |
| { |
| struct delayed_work *dwork = container_of(work, struct delayed_work, work); |
| struct hsd_info *hi = container_of(dwork, struct hsd_info, work_for_key_pressed); |
| |
| |
| if (gpio_get_value_cansleep(hi->gpio_detect) && |
| (switch_get_state(&hi->sdev)== HEADSET_WITH_MIC)) { |
| pr_warn("%s: ear jack was plugged out already!" |
| "just ignore the event.\n", __func__); |
| return; |
| } |
| |
| HSD_DBG("button_pressed \n"); |
| |
| atomic_set(&hi->btn_state, 1); |
| input_report_key(hi->input, hi->key_code, 1); |
| input_sync(hi->input); |
| } |
| |
| static void button_released(struct work_struct *work) |
| { |
| struct delayed_work *dwork = container_of( |
| work, struct delayed_work, work); |
| struct hsd_info *hi = container_of( |
| dwork, struct hsd_info, work_for_key_released); |
| |
| if (gpio_get_value_cansleep(hi->gpio_detect) && |
| (switch_get_state(&hi->sdev)== HEADSET_WITH_MIC)){ |
| pr_warn("%s: ear jack was plugged out already!" |
| "just ignore the event.\n", __func__); |
| return; |
| } |
| |
| HSD_DBG("button_released \n"); |
| |
| atomic_set(&hi->btn_state, 0); |
| input_report_key(hi->input, hi->key_code, 0); |
| input_sync(hi->input); |
| } |
| |
| static void insert_headset(struct hsd_info *hi) |
| { |
| int earjack_type; |
| |
| HSD_DBG("insert_headset"); |
| |
| if (hi->set_headset_mic_bias) |
| hi->set_headset_mic_bias(1); |
| |
| gpio_set_value_cansleep(hi->gpio_mic_en, 1); |
| |
| msleep(hi->latency_for_detection); |
| |
| earjack_type = gpio_get_value_cansleep(hi->gpio_jpole); |
| |
| if (earjack_type == HEADSET_3POLE) { |
| HSD_DBG("3 polarity earjack"); |
| |
| atomic_set(&hi->is_3_pole_or_not, 1); |
| |
| mutex_lock(&hi->mutex_lock); |
| switch_set_state(&hi->sdev, HEADSET_NO_MIC); |
| mutex_unlock(&hi->mutex_lock); |
| |
| gpio_set_value_cansleep(hi->gpio_mic_en, 0); |
| if (hi->set_headset_mic_bias) |
| hi->set_headset_mic_bias(0); |
| if (hi->set_uart_console) |
| hi->set_uart_console(0); |
| |
| input_report_switch(hi->input, SW_HEADPHONE_INSERT, 1); |
| input_sync(hi->input); |
| } else { |
| HSD_DBG("4 polarity earjack"); |
| |
| atomic_set(&hi->is_3_pole_or_not, 0); |
| |
| mutex_lock(&hi->mutex_lock); |
| switch_set_state(&hi->sdev, HEADSET_WITH_MIC); |
| mutex_unlock(&hi->mutex_lock); |
| |
| if (!atomic_read(&hi->irq_key_enabled)) { |
| HSD_DBG("enable_irq - irq_key"); |
| enable_irq(hi->irq_key); |
| |
| atomic_set(&hi->irq_key_enabled, 1); |
| } |
| if (hi->set_uart_console) |
| hi->set_uart_console(0); |
| input_report_switch(hi->input, SW_HEADPHONE_INSERT, 1); |
| input_report_switch(hi->input, SW_MICROPHONE_INSERT, 1); |
| input_sync(hi->input); |
| } |
| } |
| |
| static void remove_headset(struct hsd_info *hi) |
| { |
| int has_mic = switch_get_state(&hi->sdev); |
| |
| HSD_DBG("remove_headset"); |
| |
| gpio_set_value_cansleep(hi->gpio_mic_en, 0); |
| if (hi->set_headset_mic_bias) |
| hi->set_headset_mic_bias(0); |
| |
| atomic_set(&hi->is_3_pole_or_not, 1); |
| mutex_lock(&hi->mutex_lock); |
| switch_set_state(&hi->sdev, NO_DEVICE); |
| mutex_unlock(&hi->mutex_lock); |
| |
| if (atomic_read(&hi->irq_key_enabled)) { |
| disable_irq(hi->irq_key); |
| atomic_set(&hi->irq_key_enabled, 0); |
| } |
| |
| if (atomic_read(&hi->btn_state)) |
| #ifdef FSA8008_USE_WORK_QUEUE |
| queue_delayed_work(local_fsa8008_workqueue, |
| &(hi->work_for_key_released), hi->latency_for_key ); |
| #else |
| schedule_delayed_work(&(hi->work_for_key_released), |
| hi->latency_for_key ); |
| #endif |
| input_report_switch(hi->input, SW_HEADPHONE_INSERT, 0); |
| if (has_mic == HEADSET_WITH_MIC) |
| input_report_switch(hi->input, SW_MICROPHONE_INSERT, 0); |
| input_sync(hi->input); |
| } |
| |
| static void detect_work(struct work_struct *work) |
| { |
| int state; |
| struct delayed_work *dwork = container_of( |
| work, struct delayed_work, work); |
| struct hsd_info *hi = container_of(dwork, struct hsd_info, work); |
| |
| state = gpio_get_value_cansleep(hi->gpio_detect); |
| |
| if (state == HEADSET_REMOVE) { |
| if (switch_get_state(&hi->sdev) != NO_DEVICE) { |
| remove_headset(hi); |
| } else { |
| HSD_DBG("err_invalid_state state = %d\n", state); |
| } |
| } else { |
| |
| if (switch_get_state(&hi->sdev) == NO_DEVICE) { |
| insert_headset(hi); |
| } else { |
| HSD_DBG("err_invalid_state state = %d\n", state); |
| } |
| } |
| |
| } |
| |
| static void schedule_detect_work(struct hsd_info *hi) |
| { |
| wake_lock_timeout(&ear_hook_wake_lock, FSA8008_WAKELOCK_TIMEOUT); |
| |
| #ifdef FSA8008_USE_WORK_QUEUE |
| queue_delayed_work(local_fsa8008_workqueue, &(hi->work), |
| msecs_to_jiffies(FSA8008_DEBOUNCE_TIME)); |
| #else |
| schedule_delayed_work(&(hi->work), |
| msecs_to_jiffies(FSA8008_DEBOUNCE_TIME)); |
| #endif |
| } |
| |
| static irqreturn_t gpio_irq_handler(int irq, void *dev_id) |
| { |
| struct hsd_info *hi = (struct hsd_info *) dev_id; |
| |
| HSD_DBG("gpio_irq_handler"); |
| |
| schedule_detect_work(hi); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static irqreturn_t button_irq_handler(int irq, void *dev_id) |
| { |
| struct hsd_info *hi = (struct hsd_info *) dev_id; |
| int value; |
| |
| HSD_DBG("button_irq_handler"); |
| |
| wake_lock_timeout(&ear_hook_wake_lock, FSA8008_WAKELOCK_TIMEOUT); |
| |
| value = gpio_get_value_cansleep(hi->gpio_key); |
| |
| #ifdef FSA8008_USE_WORK_QUEUE |
| if (value) |
| queue_delayed_work(local_fsa8008_workqueue, |
| &(hi->work_for_key_pressed), |
| hi->latency_for_key ); |
| else |
| queue_delayed_work(local_fsa8008_workqueue, |
| &(hi->work_for_key_released), |
| hi->latency_for_key ); |
| #else |
| if (value) |
| schedule_delayed_work(&(hi->work_for_key_pressed), |
| hi->latency_for_key ); |
| else |
| schedule_delayed_work(&(hi->work_for_key_released), |
| hi->latency_for_key ); |
| #endif |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int hsd_gpio_init(struct hsd_info *hi) |
| { |
| int ret; |
| |
| /* initialize gpio_detect */ |
| ret = gpio_request_one(hi->gpio_detect, GPIOF_IN, "gpio_detect"); |
| if (ret < 0) { |
| pr_err("%s: Failed to gpio_request gpio%d (gpio_detect)\n", |
| __func__, hi->gpio_detect); |
| goto error_01; |
| } |
| |
| /* initialize gpio_jpole */ |
| ret = gpio_request_one(hi->gpio_jpole, GPIOF_IN, "gpio_jpole"); |
| if (ret < 0) { |
| pr_err("%s: Failed to gpio_request gpio%d (gpio_jpole)\n", |
| __func__, hi->gpio_jpole); |
| goto error_02; |
| } |
| |
| /* initialize gpio_key */ |
| ret = gpio_request_one(hi->gpio_key, GPIOF_IN, "gpio_key"); |
| if (ret < 0) { |
| pr_err("%s: Failed to gpio_request gpio%d (gpio_key)\n", |
| __func__, hi->gpio_key); |
| goto error_03; |
| } |
| |
| /* initialize gpio_mic_en */ |
| ret = gpio_request_one(hi->gpio_mic_en, GPIOF_OUT_INIT_LOW, |
| "gpio_mic_en"); |
| if (ret < 0) { |
| pr_err("%s: Failed to gpio_request gpio%d (gpio_mic_en)\n", |
| __func__, hi->gpio_mic_en); |
| goto error_04; |
| } |
| |
| /* initialize gpio_mic_bias_en */ |
| if (gpio_is_valid(hi->gpio_mic_bias_en)) { |
| ret = gpio_request_one(hi->gpio_mic_bias_en, |
| GPIOF_OUT_INIT_LOW, "gpio_mic_bias_en"); |
| if (ret < 0) { |
| pr_err("%s: Failed to gpio_request gpio%d " |
| "(gpio_mic_bias_en)\n", |
| __func__, hi->gpio_mic_en); |
| goto error_05; |
| } |
| } |
| |
| return 0; |
| |
| error_05: |
| gpio_free(hi->gpio_mic_en); |
| error_04: |
| gpio_free(hi->gpio_key); |
| error_03: |
| gpio_free(hi->gpio_jpole); |
| error_02: |
| gpio_free(hi->gpio_detect); |
| error_01: |
| return ret; |
| } |
| |
| static void hsd_gpio_free(struct hsd_info *hi) |
| { |
| if (gpio_is_valid(hi->gpio_mic_bias_en)) |
| gpio_free(hi->gpio_mic_bias_en); |
| gpio_free(hi->gpio_mic_en); |
| gpio_free(hi->gpio_key); |
| gpio_free(hi->gpio_jpole); |
| gpio_free(hi->gpio_detect); |
| } |
| |
| static int hsd_probe(struct platform_device *pdev) |
| { |
| int ret = 0; |
| struct fsa8008_platform_data *pdata = pdev->dev.platform_data; |
| struct hsd_info *hi; |
| |
| HSD_DBG("hsd_probe"); |
| |
| if (!pdata) { |
| pr_err("%s: no pdata\n", __func__); |
| return -ENODEV; |
| } |
| |
| hi = kzalloc(sizeof(struct hsd_info), GFP_KERNEL); |
| if (NULL == hi) { |
| pr_err("%s: out of memory\n", __func__); |
| return -ENOMEM; |
| } |
| |
| hi->key_code = pdata->key_code; |
| |
| platform_set_drvdata(pdev, hi); |
| |
| atomic_set(&hi->btn_state, 0); |
| atomic_set(&hi->is_3_pole_or_not, 1); |
| |
| hi->gpio_detect = pdata->gpio_detect; |
| hi->gpio_detect_can_wakeup = pdata->gpio_detect_can_wakeup; |
| hi->gpio_mic_en = pdata->gpio_mic_en; |
| hi->gpio_mic_bias_en = pdata->gpio_mic_bias_en; |
| hi->gpio_jpole = pdata->gpio_jpole; |
| hi->gpio_key = pdata->gpio_key; |
| hi->set_headset_mic_bias = pdata->set_headset_mic_bias; |
| hi->set_uart_console = pdata->set_uart_console; |
| hi->latency_for_detection = pdata->latency_for_detection; |
| hi->latency_for_key = msecs_to_jiffies(FSA8008_KEY_LATENCY_TIME); |
| |
| mutex_init(&hi->mutex_lock); |
| |
| INIT_DELAYED_WORK(&hi->work, detect_work); |
| INIT_DELAYED_WORK(&hi->work_for_key_pressed, button_pressed); |
| INIT_DELAYED_WORK(&hi->work_for_key_released, button_released); |
| |
| if (hsd_gpio_init(hi) < 0) |
| goto error_01; |
| |
| /* initialize irq of gpio_jpole */ |
| hi->irq_detect = gpio_to_irq(hi->gpio_detect); |
| HSD_DBG("hi->irq_detect = %d\n", hi->irq_detect); |
| if (hi->irq_detect < 0) { |
| pr_err("%s: Failed to get interrupt number\n", __func__); |
| ret = hi->irq_detect; |
| goto error_02; |
| } |
| |
| ret = request_threaded_irq(hi->irq_detect, NULL, gpio_irq_handler, |
| IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, |
| pdev->name, hi); |
| if (ret) { |
| pr_err("%s: failed to request button irq\n", __func__); |
| goto error_02; |
| } |
| |
| if (hi->gpio_detect_can_wakeup) { |
| ret = irq_set_irq_wake(hi->irq_detect, 1); |
| if (ret < 0) { |
| pr_err("%s: Failed to set irq_detect interrupt wake\n", |
| __func__); |
| goto error_03; |
| } |
| } |
| |
| /* initialize irq of gpio_key */ |
| hi->irq_key = gpio_to_irq(hi->gpio_key); |
| HSD_DBG("hi->irq_key = %d\n", hi->irq_key); |
| if (hi->irq_key < 0) { |
| pr_err("%s: Failed to get interrupt number\n", __func__); |
| ret = hi->irq_key; |
| goto error_03; |
| } |
| |
| ret = request_threaded_irq(hi->irq_key, NULL, button_irq_handler, |
| IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, |
| pdev->name, hi); |
| if (ret) { |
| pr_err("%s: failed to request button irq\n", __func__); |
| goto error_03; |
| } |
| |
| disable_irq(hi->irq_key); |
| |
| ret = irq_set_irq_wake(hi->irq_key, 1); |
| if (ret < 0) { |
| pr_err("%s: Failed to set irq_key interrupt wake\n", __func__); |
| goto error_04; |
| } |
| |
| /* initialize switch device */ |
| hi->sdev.name = pdata->switch_name; |
| hi->sdev.print_state = hsd_print_state; |
| hi->sdev.print_name = hsd_print_name; |
| |
| ret = switch_dev_register(&hi->sdev); |
| if (ret < 0) { |
| pr_err("%s: Failed to register switch device\n", __func__); |
| goto error_04; |
| } |
| |
| /* initialize input device */ |
| hi->input = input_allocate_device(); |
| if (!hi->input) { |
| pr_err("%s: Failed to allocate input device\n", __func__); |
| ret = -ENOMEM; |
| goto error_05; |
| } |
| |
| hi->input->name = pdata->keypad_name; |
| |
| hi->input->id.vendor = 0x0001; |
| hi->input->id.product = 1; |
| hi->input->id.version = 1; |
| |
| set_bit(EV_SYN, hi->input->evbit); |
| set_bit(EV_KEY, hi->input->evbit); |
| set_bit(EV_SW, hi->input->evbit); |
| set_bit(hi->key_code, hi->input->keybit); |
| set_bit(SW_HEADPHONE_INSERT, hi->input->swbit); |
| set_bit(SW_MICROPHONE_INSERT, hi->input->swbit); |
| |
| ret = input_register_device(hi->input); |
| if (ret) { |
| pr_err("%s: Failed to register input device\n", __func__); |
| goto error_06; |
| } |
| |
| if (!gpio_get_value_cansleep(hi->gpio_detect)) { |
| #ifdef FSA8008_USE_WORK_QUEUE |
| /* to detect in initialization with eacjack insertion */ |
| queue_delayed_work(local_fsa8008_workqueue, &(hi->work), 0); |
| #else |
| /* to detect in initialization with eacjack insertion */ |
| schedule_delayed_work(&(hi->work), 0); |
| #endif |
| } |
| |
| return ret; |
| |
| error_06: |
| input_free_device(hi->input); |
| error_05: |
| switch_dev_unregister(&hi->sdev); |
| error_04: |
| free_irq(hi->irq_key, 0); |
| error_03: |
| free_irq(hi->irq_detect, 0); |
| error_02: |
| hsd_gpio_free(hi); |
| error_01: |
| mutex_destroy(&hi->mutex_lock); |
| kfree(hi); |
| |
| return ret; |
| } |
| |
| static int hsd_remove(struct platform_device *pdev) |
| { |
| struct hsd_info *hi = (struct hsd_info *)platform_get_drvdata(pdev); |
| |
| HSD_DBG("hsd_remove"); |
| |
| if (switch_get_state(&hi->sdev)) |
| remove_headset(hi); |
| |
| input_unregister_device(hi->input); |
| switch_dev_unregister(&hi->sdev); |
| |
| free_irq(hi->irq_key, 0); |
| free_irq(hi->irq_detect, 0); |
| |
| hsd_gpio_free(hi); |
| |
| mutex_destroy(&hi->mutex_lock); |
| kfree(hi); |
| |
| return 0; |
| } |
| |
| static int hsd_suspend(struct device *dev) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct hsd_info *hi = platform_get_drvdata(pdev); |
| |
| HSD_DBG("hsd_suspend"); |
| |
| if (!hi->gpio_detect_can_wakeup) { |
| disable_irq(hi->irq_detect); |
| hi->saved_detect = gpio_get_value(hi->gpio_detect); |
| } |
| |
| return 0; |
| } |
| |
| static int hsd_resume(struct device *dev) |
| { |
| |
| struct platform_device *pdev = to_platform_device(dev); |
| struct hsd_info *hi = platform_get_drvdata(pdev); |
| int detect = 0; |
| |
| HSD_DBG("hsd_resume"); |
| |
| detect = gpio_get_value(hi->gpio_detect); |
| if (HEADSET_INSERT == detect) |
| if (hi->set_uart_console) |
| hi->set_uart_console(0); |
| |
| if (!hi->gpio_detect_can_wakeup) { |
| enable_irq(hi->irq_detect); |
| if (hi->saved_detect != detect) |
| schedule_detect_work(hi); |
| } |
| |
| return 0; |
| } |
| |
| static const struct dev_pm_ops hsd_pm_ops = { |
| SET_SYSTEM_SLEEP_PM_OPS(hsd_suspend, hsd_resume) |
| }; |
| |
| static struct platform_driver hsd_driver = { |
| .probe = hsd_probe, |
| .remove = hsd_remove, |
| .driver = { |
| .name = "fsa8008", |
| .owner = THIS_MODULE, |
| .pm = &hsd_pm_ops, |
| }, |
| }; |
| |
| static int __init hsd_init(void) |
| { |
| int ret; |
| |
| HSD_DBG("hsd_init"); |
| |
| #ifdef FSA8008_USE_WORK_QUEUE |
| local_fsa8008_workqueue = create_workqueue("fsa8008"); |
| if (!local_fsa8008_workqueue) { |
| pr_err("%s: out of memory\n", __func__); |
| return -ENOMEM; |
| } |
| #endif |
| |
| ret = platform_driver_register(&hsd_driver); |
| if (ret< 0) { |
| pr_err("%s: Fail to register platform driver\n", __func__); |
| goto err; |
| } |
| |
| wake_lock_init(&ear_hook_wake_lock, WAKE_LOCK_SUSPEND, "ear_hook"); |
| |
| return ret; |
| |
| err: |
| #ifdef FSA8008_USE_WORK_QUEUE |
| if (local_fsa8008_workqueue) |
| destroy_workqueue(local_fsa8008_workqueue); |
| local_fsa8008_workqueue = NULL; |
| #endif |
| return ret; |
| } |
| |
| static void __exit hsd_exit(void) |
| { |
| HSD_DBG("hsd_exit"); |
| |
| #ifdef FSA8008_USE_WORK_QUEUE |
| if (local_fsa8008_workqueue) |
| destroy_workqueue(local_fsa8008_workqueue); |
| local_fsa8008_workqueue = NULL; |
| #endif |
| |
| platform_driver_unregister(&hsd_driver); |
| wake_lock_destroy(&ear_hook_wake_lock); |
| } |
| |
| /* to make init after pmicxxxx module */ |
| late_initcall_sync(hsd_init); |
| module_exit(hsd_exit); |
| |
| MODULE_AUTHOR("Yoon Gi Souk <gisouk.yoon@lge.com>"); |
| MODULE_DESCRIPTION("FSA8008 Headset detection driver"); |
| MODULE_LICENSE("GPL"); |