1189 lines
30 KiB
C
1189 lines
30 KiB
C
/* Copyright (c) 2010-2011, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "%s: " fmt, __func__
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/init.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/input.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/gpio.h>
|
|
#include <linux/switch.h>
|
|
#include <linux/pm.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/hrtimer.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/regulator/consumer.h>
|
|
|
|
#include <linux/mfd/pm8xxx/core.h>
|
|
#include <linux/pmic8058-othc.h>
|
|
#include <linux/msm_adc.h>
|
|
|
|
#define PM8058_OTHC_LOW_CURR_MASK 0xF0
|
|
#define PM8058_OTHC_HIGH_CURR_MASK 0x0F
|
|
#define PM8058_OTHC_EN_SIG_MASK 0x3F
|
|
#define PM8058_OTHC_HYST_PREDIV_MASK 0xC7
|
|
#define PM8058_OTHC_CLK_PREDIV_MASK 0xF8
|
|
#define PM8058_OTHC_HYST_CLK_MASK 0x0F
|
|
#define PM8058_OTHC_PERIOD_CLK_MASK 0xF0
|
|
|
|
#define PM8058_OTHC_LOW_CURR_SHIFT 0x4
|
|
#define PM8058_OTHC_EN_SIG_SHIFT 0x6
|
|
#define PM8058_OTHC_HYST_PREDIV_SHIFT 0x3
|
|
#define PM8058_OTHC_HYST_CLK_SHIFT 0x4
|
|
|
|
#define OTHC_GPIO_MAX_LEN 25
|
|
|
|
struct pm8058_othc {
|
|
bool othc_sw_state;
|
|
bool switch_reject;
|
|
bool othc_support_n_switch;
|
|
bool accessory_support;
|
|
bool accessories_adc_support;
|
|
int othc_base;
|
|
int othc_irq_sw;
|
|
int othc_irq_ir;
|
|
int othc_ir_state;
|
|
int num_accessories;
|
|
int curr_accessory_code;
|
|
int curr_accessory;
|
|
int video_out_gpio;
|
|
u32 sw_key_code;
|
|
u32 accessories_adc_channel;
|
|
int ir_gpio;
|
|
unsigned long switch_debounce_ms;
|
|
unsigned long detection_delay_ms;
|
|
void *adc_handle;
|
|
void *accessory_adc_handle;
|
|
spinlock_t lock;
|
|
struct device *dev;
|
|
struct regulator *othc_vreg;
|
|
struct input_dev *othc_ipd;
|
|
struct switch_dev othc_sdev;
|
|
struct pmic8058_othc_config_pdata *othc_pdata;
|
|
struct othc_accessory_info *accessory_info;
|
|
struct hrtimer timer;
|
|
struct othc_n_switch_config *switch_config;
|
|
struct work_struct switch_work;
|
|
struct delayed_work detect_work;
|
|
struct delayed_work hs_work;
|
|
};
|
|
|
|
static struct pm8058_othc *config[OTHC_MICBIAS_MAX];
|
|
|
|
static void hs_worker(struct work_struct *work)
|
|
{
|
|
int rc;
|
|
struct pm8058_othc *dd =
|
|
container_of(work, struct pm8058_othc, hs_work.work);
|
|
|
|
rc = gpio_get_value_cansleep(dd->ir_gpio);
|
|
if (rc < 0) {
|
|
pr_err("Unable to read IR GPIO\n");
|
|
enable_irq(dd->othc_irq_ir);
|
|
return;
|
|
}
|
|
|
|
dd->othc_ir_state = !rc;
|
|
schedule_delayed_work(&dd->detect_work,
|
|
msecs_to_jiffies(dd->detection_delay_ms));
|
|
}
|
|
|
|
static irqreturn_t ir_gpio_irq(int irq, void *dev_id)
|
|
{
|
|
unsigned long flags;
|
|
struct pm8058_othc *dd = dev_id;
|
|
|
|
spin_lock_irqsave(&dd->lock, flags);
|
|
/* Enable the switch reject flag */
|
|
dd->switch_reject = true;
|
|
spin_unlock_irqrestore(&dd->lock, flags);
|
|
|
|
/* Start the HR timer if one is not active */
|
|
if (hrtimer_active(&dd->timer))
|
|
hrtimer_cancel(&dd->timer);
|
|
|
|
hrtimer_start(&dd->timer,
|
|
ktime_set((dd->switch_debounce_ms / 1000),
|
|
(dd->switch_debounce_ms % 1000) * 1000000), HRTIMER_MODE_REL);
|
|
|
|
/* disable irq, this gets enabled in the workqueue */
|
|
disable_irq_nosync(dd->othc_irq_ir);
|
|
schedule_delayed_work(&dd->hs_work, 0);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
/*
|
|
* The API pm8058_micbias_enable() allows to configure
|
|
* the MIC_BIAS. Only the lines which are not used for
|
|
* headset detection can be configured using this API.
|
|
* The API returns an error code if it fails to configure
|
|
* the specified MIC_BIAS line, else it returns 0.
|
|
*/
|
|
int pm8058_micbias_enable(enum othc_micbias micbias,
|
|
enum othc_micbias_enable enable)
|
|
{
|
|
int rc;
|
|
u8 reg;
|
|
struct pm8058_othc *dd = config[micbias];
|
|
|
|
if (dd == NULL) {
|
|
pr_err("MIC_BIAS not registered, cannot enable\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (dd->othc_pdata->micbias_capability != OTHC_MICBIAS) {
|
|
pr_err("MIC_BIAS enable capability not supported\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
rc = pm8xxx_readb(dd->dev->parent, dd->othc_base + 1, ®);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
reg &= PM8058_OTHC_EN_SIG_MASK;
|
|
reg |= (enable << PM8058_OTHC_EN_SIG_SHIFT);
|
|
|
|
rc = pm8xxx_writeb(dd->dev->parent, dd->othc_base + 1, reg);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 write failed\n");
|
|
return rc;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
EXPORT_SYMBOL(pm8058_micbias_enable);
|
|
|
|
int pm8058_othc_svideo_enable(enum othc_micbias micbias, bool enable)
|
|
{
|
|
struct pm8058_othc *dd = config[micbias];
|
|
|
|
if (dd == NULL) {
|
|
pr_err("MIC_BIAS not registered, cannot enable\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (dd->othc_pdata->micbias_capability != OTHC_MICBIAS_HSED) {
|
|
pr_err("MIC_BIAS enable capability not supported\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (dd->accessories_adc_support) {
|
|
/* GPIO state for MIC_IN = 0, SVIDEO = 1 */
|
|
gpio_set_value_cansleep(dd->video_out_gpio, !!enable);
|
|
if (enable) {
|
|
pr_debug("Enable the video path\n");
|
|
switch_set_state(&dd->othc_sdev, dd->curr_accessory);
|
|
input_report_switch(dd->othc_ipd,
|
|
dd->curr_accessory_code, 1);
|
|
input_sync(dd->othc_ipd);
|
|
} else {
|
|
pr_debug("Disable the video path\n");
|
|
switch_set_state(&dd->othc_sdev, 0);
|
|
input_report_switch(dd->othc_ipd,
|
|
dd->curr_accessory_code, 0);
|
|
input_sync(dd->othc_ipd);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(pm8058_othc_svideo_enable);
|
|
|
|
#ifdef CONFIG_PM
|
|
static int pm8058_othc_suspend(struct device *dev)
|
|
{
|
|
int rc = 0;
|
|
struct pm8058_othc *dd = dev_get_drvdata(dev);
|
|
|
|
if (dd->othc_pdata->micbias_capability == OTHC_MICBIAS_HSED) {
|
|
if (device_may_wakeup(dev)) {
|
|
enable_irq_wake(dd->othc_irq_sw);
|
|
enable_irq_wake(dd->othc_irq_ir);
|
|
}
|
|
}
|
|
|
|
if (!device_may_wakeup(dev)) {
|
|
rc = regulator_disable(dd->othc_vreg);
|
|
if (rc)
|
|
pr_err("othc micbais power off failed\n");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int pm8058_othc_resume(struct device *dev)
|
|
{
|
|
int rc = 0;
|
|
struct pm8058_othc *dd = dev_get_drvdata(dev);
|
|
|
|
if (dd->othc_pdata->micbias_capability == OTHC_MICBIAS_HSED) {
|
|
if (device_may_wakeup(dev)) {
|
|
disable_irq_wake(dd->othc_irq_sw);
|
|
disable_irq_wake(dd->othc_irq_ir);
|
|
}
|
|
}
|
|
|
|
if (!device_may_wakeup(dev)) {
|
|
rc = regulator_enable(dd->othc_vreg);
|
|
if (rc)
|
|
pr_err("othc micbais power on failed\n");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
static struct dev_pm_ops pm8058_othc_pm_ops = {
|
|
.suspend = pm8058_othc_suspend,
|
|
.resume = pm8058_othc_resume,
|
|
};
|
|
#endif
|
|
|
|
static int __devexit pm8058_othc_remove(struct platform_device *pd)
|
|
{
|
|
struct pm8058_othc *dd = platform_get_drvdata(pd);
|
|
|
|
pm_runtime_set_suspended(&pd->dev);
|
|
pm_runtime_disable(&pd->dev);
|
|
|
|
if (dd->othc_pdata->micbias_capability == OTHC_MICBIAS_HSED) {
|
|
device_init_wakeup(&pd->dev, 0);
|
|
if (dd->othc_support_n_switch == true) {
|
|
adc_channel_close(dd->adc_handle);
|
|
cancel_work_sync(&dd->switch_work);
|
|
}
|
|
|
|
if (dd->accessory_support == true) {
|
|
int i;
|
|
for (i = 0; i < dd->num_accessories; i++) {
|
|
if (dd->accessory_info[i].detect_flags &
|
|
OTHC_GPIO_DETECT)
|
|
gpio_free(dd->accessory_info[i].gpio);
|
|
}
|
|
}
|
|
cancel_delayed_work_sync(&dd->detect_work);
|
|
cancel_delayed_work_sync(&dd->hs_work);
|
|
free_irq(dd->othc_irq_sw, dd);
|
|
free_irq(dd->othc_irq_ir, dd);
|
|
if (dd->ir_gpio != -1)
|
|
gpio_free(dd->ir_gpio);
|
|
input_unregister_device(dd->othc_ipd);
|
|
}
|
|
regulator_disable(dd->othc_vreg);
|
|
regulator_put(dd->othc_vreg);
|
|
|
|
kfree(dd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum hrtimer_restart pm8058_othc_timer(struct hrtimer *timer)
|
|
{
|
|
unsigned long flags;
|
|
struct pm8058_othc *dd = container_of(timer,
|
|
struct pm8058_othc, timer);
|
|
|
|
spin_lock_irqsave(&dd->lock, flags);
|
|
dd->switch_reject = false;
|
|
spin_unlock_irqrestore(&dd->lock, flags);
|
|
|
|
return HRTIMER_NORESTART;
|
|
}
|
|
|
|
static void othc_report_switch(struct pm8058_othc *dd, u32 res)
|
|
{
|
|
u8 i;
|
|
struct othc_switch_info *sw_info = dd->switch_config->switch_info;
|
|
|
|
for (i = 0; i < dd->switch_config->num_keys; i++) {
|
|
if (res >= sw_info[i].min_adc_threshold &&
|
|
res <= sw_info[i].max_adc_threshold) {
|
|
dd->othc_sw_state = true;
|
|
dd->sw_key_code = sw_info[i].key_code;
|
|
input_report_key(dd->othc_ipd, sw_info[i].key_code, 1);
|
|
input_sync(dd->othc_ipd);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If the switch is not present in a specified ADC range
|
|
* report a default switch press.
|
|
*/
|
|
if (dd->switch_config->default_sw_en) {
|
|
dd->othc_sw_state = true;
|
|
dd->sw_key_code =
|
|
sw_info[dd->switch_config->default_sw_idx].key_code;
|
|
input_report_key(dd->othc_ipd, dd->sw_key_code, 1);
|
|
input_sync(dd->othc_ipd);
|
|
}
|
|
}
|
|
|
|
static void switch_work_f(struct work_struct *work)
|
|
{
|
|
int rc, i;
|
|
u32 res = 0;
|
|
struct adc_chan_result adc_result;
|
|
struct pm8058_othc *dd =
|
|
container_of(work, struct pm8058_othc, switch_work);
|
|
DECLARE_COMPLETION_ONSTACK(adc_wait);
|
|
u8 num_adc_samples = dd->switch_config->num_adc_samples;
|
|
|
|
/* sleep for settling time */
|
|
msleep(dd->switch_config->voltage_settling_time_ms);
|
|
|
|
for (i = 0; i < num_adc_samples; i++) {
|
|
rc = adc_channel_request_conv(dd->adc_handle, &adc_wait);
|
|
if (rc) {
|
|
pr_err("adc_channel_request_conv failed\n");
|
|
goto bail_out;
|
|
}
|
|
rc = wait_for_completion_interruptible(&adc_wait);
|
|
if (rc) {
|
|
pr_err("wait_for_completion_interruptible failed\n");
|
|
goto bail_out;
|
|
}
|
|
rc = adc_channel_read_result(dd->adc_handle, &adc_result);
|
|
if (rc) {
|
|
pr_err("adc_channel_read_result failed\n");
|
|
goto bail_out;
|
|
}
|
|
res += adc_result.physical;
|
|
}
|
|
bail_out:
|
|
if (i == num_adc_samples && num_adc_samples != 0) {
|
|
res /= num_adc_samples;
|
|
othc_report_switch(dd, res);
|
|
} else
|
|
pr_err("Insufficient ADC samples\n");
|
|
|
|
enable_irq(dd->othc_irq_sw);
|
|
}
|
|
|
|
static int accessory_adc_detect(struct pm8058_othc *dd, int accessory)
|
|
{
|
|
int rc;
|
|
u32 res;
|
|
struct adc_chan_result accessory_adc_result;
|
|
DECLARE_COMPLETION_ONSTACK(accessory_adc_wait);
|
|
|
|
rc = adc_channel_request_conv(dd->accessory_adc_handle,
|
|
&accessory_adc_wait);
|
|
if (rc) {
|
|
pr_err("adc_channel_request_conv failed\n");
|
|
goto adc_failed;
|
|
}
|
|
rc = wait_for_completion_interruptible(&accessory_adc_wait);
|
|
if (rc) {
|
|
pr_err("wait_for_completion_interruptible failed\n");
|
|
goto adc_failed;
|
|
}
|
|
rc = adc_channel_read_result(dd->accessory_adc_handle,
|
|
&accessory_adc_result);
|
|
if (rc) {
|
|
pr_err("adc_channel_read_result failed\n");
|
|
goto adc_failed;
|
|
}
|
|
|
|
res = accessory_adc_result.physical;
|
|
|
|
if (res >= dd->accessory_info[accessory].adc_thres.min_threshold &&
|
|
res <= dd->accessory_info[accessory].adc_thres.max_threshold) {
|
|
pr_debug("Accessory on ADC detected!, ADC Value = %u\n", res);
|
|
return 1;
|
|
}
|
|
|
|
adc_failed:
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int pm8058_accessory_report(struct pm8058_othc *dd, int status)
|
|
{
|
|
int i, rc, detected = 0;
|
|
u8 micbias_status, switch_status;
|
|
|
|
if (dd->accessory_support == false) {
|
|
/* Report default headset */
|
|
switch_set_state(&dd->othc_sdev, !!status);
|
|
input_report_switch(dd->othc_ipd, SW_HEADPHONE_INSERT,
|
|
!!status);
|
|
input_sync(dd->othc_ipd);
|
|
return 0;
|
|
}
|
|
|
|
/* For accessory */
|
|
if (dd->accessory_support == true && status == 0) {
|
|
/* Report removal of the accessory. */
|
|
|
|
/*
|
|
* If the current accessory is video cable, reject the removal
|
|
* interrupt.
|
|
*/
|
|
pr_info("Accessory [%d] removed\n", dd->curr_accessory);
|
|
if (dd->curr_accessory == OTHC_SVIDEO_OUT)
|
|
return 0;
|
|
|
|
switch_set_state(&dd->othc_sdev, 0);
|
|
input_report_switch(dd->othc_ipd, dd->curr_accessory_code, 0);
|
|
input_sync(dd->othc_ipd);
|
|
return 0;
|
|
}
|
|
|
|
if (dd->ir_gpio < 0) {
|
|
/* Check the MIC_BIAS status */
|
|
rc = pm8xxx_read_irq_stat(dd->dev->parent, dd->othc_irq_ir);
|
|
if (rc < 0) {
|
|
pr_err("Unable to read IR status from PMIC\n");
|
|
goto fail_ir_accessory;
|
|
}
|
|
micbias_status = !!rc;
|
|
} else {
|
|
rc = gpio_get_value_cansleep(dd->ir_gpio);
|
|
if (rc < 0) {
|
|
pr_err("Unable to read IR status from GPIO\n");
|
|
goto fail_ir_accessory;
|
|
}
|
|
micbias_status = !rc;
|
|
}
|
|
|
|
/* Check the switch status */
|
|
rc = pm8xxx_read_irq_stat(dd->dev->parent, dd->othc_irq_sw);
|
|
if (rc < 0) {
|
|
pr_err("Unable to read SWITCH status\n");
|
|
goto fail_ir_accessory;
|
|
}
|
|
switch_status = !!rc;
|
|
|
|
/* Loop through to check which accessory is connected */
|
|
for (i = 0; i < dd->num_accessories; i++) {
|
|
detected = 0;
|
|
if (dd->accessory_info[i].enabled == false)
|
|
continue;
|
|
|
|
if (dd->accessory_info[i].detect_flags & OTHC_MICBIAS_DETECT) {
|
|
if (micbias_status)
|
|
detected = 1;
|
|
else
|
|
continue;
|
|
}
|
|
if (dd->accessory_info[i].detect_flags & OTHC_SWITCH_DETECT) {
|
|
if (switch_status)
|
|
detected = 1;
|
|
else
|
|
continue;
|
|
}
|
|
if (dd->accessory_info[i].detect_flags & OTHC_GPIO_DETECT) {
|
|
rc = gpio_get_value_cansleep(
|
|
dd->accessory_info[i].gpio);
|
|
if (rc < 0)
|
|
continue;
|
|
|
|
if (rc ^ dd->accessory_info[i].active_low)
|
|
detected = 1;
|
|
else
|
|
continue;
|
|
}
|
|
if (dd->accessory_info[i].detect_flags & OTHC_ADC_DETECT)
|
|
detected = accessory_adc_detect(dd, i);
|
|
|
|
if (detected)
|
|
break;
|
|
}
|
|
|
|
if (detected) {
|
|
dd->curr_accessory = dd->accessory_info[i].accessory;
|
|
dd->curr_accessory_code = dd->accessory_info[i].key_code;
|
|
|
|
/* if Video out cable detected enable the video path*/
|
|
if (dd->curr_accessory == OTHC_SVIDEO_OUT) {
|
|
pm8058_othc_svideo_enable(
|
|
dd->othc_pdata->micbias_select, true);
|
|
|
|
} else {
|
|
switch_set_state(&dd->othc_sdev, dd->curr_accessory);
|
|
input_report_switch(dd->othc_ipd,
|
|
dd->curr_accessory_code, 1);
|
|
input_sync(dd->othc_ipd);
|
|
}
|
|
pr_info("Accessory [%d] inserted\n", dd->curr_accessory);
|
|
} else
|
|
pr_info("Unable to detect accessory. False interrupt!\n");
|
|
|
|
return 0;
|
|
|
|
fail_ir_accessory:
|
|
return rc;
|
|
}
|
|
|
|
static void detect_work_f(struct work_struct *work)
|
|
{
|
|
int rc;
|
|
struct pm8058_othc *dd =
|
|
container_of(work, struct pm8058_othc, detect_work.work);
|
|
|
|
/* Accessory has been inserted */
|
|
rc = pm8058_accessory_report(dd, 1);
|
|
if (rc)
|
|
pr_err("Accessory insertion could not be detected\n");
|
|
|
|
enable_irq(dd->othc_irq_ir);
|
|
}
|
|
|
|
/*
|
|
* The pm8058_no_sw detects the switch press and release operation.
|
|
* The odd number call is press and even number call is release.
|
|
* The current state of the button is maintained in othc_sw_state variable.
|
|
* This isr gets called only for NO type headsets.
|
|
*/
|
|
static irqreturn_t pm8058_no_sw(int irq, void *dev_id)
|
|
{
|
|
int level;
|
|
struct pm8058_othc *dd = dev_id;
|
|
unsigned long flags;
|
|
|
|
/* Check if headset has been inserted, else return */
|
|
if (!dd->othc_ir_state)
|
|
return IRQ_HANDLED;
|
|
|
|
spin_lock_irqsave(&dd->lock, flags);
|
|
if (dd->switch_reject == true) {
|
|
pr_debug("Rejected switch interrupt\n");
|
|
spin_unlock_irqrestore(&dd->lock, flags);
|
|
return IRQ_HANDLED;
|
|
}
|
|
spin_unlock_irqrestore(&dd->lock, flags);
|
|
|
|
level = pm8xxx_read_irq_stat(dd->dev->parent, dd->othc_irq_sw);
|
|
if (level < 0) {
|
|
pr_err("Unable to read IRQ status register\n");
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
if (dd->othc_support_n_switch == true) {
|
|
if (level == 0) {
|
|
dd->othc_sw_state = false;
|
|
input_report_key(dd->othc_ipd, dd->sw_key_code, 0);
|
|
input_sync(dd->othc_ipd);
|
|
} else {
|
|
disable_irq_nosync(dd->othc_irq_sw);
|
|
schedule_work(&dd->switch_work);
|
|
}
|
|
return IRQ_HANDLED;
|
|
}
|
|
/*
|
|
* It is necessary to check the software state and the hardware state
|
|
* to make sure that the residual interrupt after the debounce time does
|
|
* not disturb the software state machine.
|
|
*/
|
|
if (level == 1 && dd->othc_sw_state == false) {
|
|
/* Switch has been pressed */
|
|
dd->othc_sw_state = true;
|
|
input_report_key(dd->othc_ipd, KEY_MEDIA, 1);
|
|
} else if (level == 0 && dd->othc_sw_state == true) {
|
|
/* Switch has been released */
|
|
dd->othc_sw_state = false;
|
|
input_report_key(dd->othc_ipd, KEY_MEDIA, 0);
|
|
}
|
|
input_sync(dd->othc_ipd);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*
|
|
* The pm8058_nc_ir detects insert / remove of the headset (for NO),
|
|
* The current state of the headset is maintained in othc_ir_state variable.
|
|
* Due to a hardware bug, false switch interrupts are seen during headset
|
|
* insert. This is handled in the software by rejecting the switch interrupts
|
|
* for a small period of time after the headset has been inserted.
|
|
*/
|
|
static irqreturn_t pm8058_nc_ir(int irq, void *dev_id)
|
|
{
|
|
unsigned long flags, rc;
|
|
struct pm8058_othc *dd = dev_id;
|
|
|
|
spin_lock_irqsave(&dd->lock, flags);
|
|
/* Enable the switch reject flag */
|
|
dd->switch_reject = true;
|
|
spin_unlock_irqrestore(&dd->lock, flags);
|
|
|
|
/* Start the HR timer if one is not active */
|
|
if (hrtimer_active(&dd->timer))
|
|
hrtimer_cancel(&dd->timer);
|
|
|
|
hrtimer_start(&dd->timer,
|
|
ktime_set((dd->switch_debounce_ms / 1000),
|
|
(dd->switch_debounce_ms % 1000) * 1000000), HRTIMER_MODE_REL);
|
|
|
|
|
|
/* Check the MIC_BIAS status, to check if inserted or removed */
|
|
rc = pm8xxx_read_irq_stat(dd->dev->parent, dd->othc_irq_ir);
|
|
if (rc < 0) {
|
|
pr_err("Unable to read IR status\n");
|
|
goto fail_ir;
|
|
}
|
|
|
|
dd->othc_ir_state = rc;
|
|
if (dd->othc_ir_state) {
|
|
/* disable irq, this gets enabled in the workqueue */
|
|
disable_irq_nosync(dd->othc_irq_ir);
|
|
/* Accessory has been inserted, report with detection delay */
|
|
schedule_delayed_work(&dd->detect_work,
|
|
msecs_to_jiffies(dd->detection_delay_ms));
|
|
} else {
|
|
/* Accessory has been removed, report removal immediately */
|
|
rc = pm8058_accessory_report(dd, 0);
|
|
if (rc)
|
|
pr_err("Accessory removal could not be detected\n");
|
|
/* Clear existing switch state */
|
|
dd->othc_sw_state = false;
|
|
}
|
|
|
|
fail_ir:
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int pm8058_configure_micbias(struct pm8058_othc *dd)
|
|
{
|
|
int rc;
|
|
u8 reg, value;
|
|
u32 value1;
|
|
u16 base_addr = dd->othc_base;
|
|
struct hsed_bias_config *hsed_config =
|
|
dd->othc_pdata->hsed_config->hsed_bias_config;
|
|
|
|
/* Intialize the OTHC module */
|
|
/* Control Register 1*/
|
|
rc = pm8xxx_readb(dd->dev->parent, base_addr, ®);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
/* set iDAC high current threshold */
|
|
value = (hsed_config->othc_highcurr_thresh_uA / 100) - 2;
|
|
reg = (reg & PM8058_OTHC_HIGH_CURR_MASK) | value;
|
|
|
|
rc = pm8xxx_writeb(dd->dev->parent, base_addr, reg);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
/* Control register 2*/
|
|
rc = pm8xxx_readb(dd->dev->parent, base_addr + 1, ®);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
value = dd->othc_pdata->micbias_enable;
|
|
reg &= PM8058_OTHC_EN_SIG_MASK;
|
|
reg |= (value << PM8058_OTHC_EN_SIG_SHIFT);
|
|
|
|
value = 0;
|
|
value1 = (hsed_config->othc_hyst_prediv_us << 10) / USEC_PER_SEC;
|
|
while (value1 != 0) {
|
|
value1 = value1 >> 1;
|
|
value++;
|
|
}
|
|
if (value > 7) {
|
|
pr_err("Invalid input argument - othc_hyst_prediv_us\n");
|
|
return -EINVAL;
|
|
}
|
|
reg &= PM8058_OTHC_HYST_PREDIV_MASK;
|
|
reg |= (value << PM8058_OTHC_HYST_PREDIV_SHIFT);
|
|
|
|
value = 0;
|
|
value1 = (hsed_config->othc_period_clkdiv_us << 10) / USEC_PER_SEC;
|
|
while (value1 != 1) {
|
|
value1 = value1 >> 1;
|
|
value++;
|
|
}
|
|
if (value > 8) {
|
|
pr_err("Invalid input argument - othc_period_clkdiv_us\n");
|
|
return -EINVAL;
|
|
}
|
|
reg = (reg & PM8058_OTHC_CLK_PREDIV_MASK) | (value - 1);
|
|
|
|
rc = pm8xxx_writeb(dd->dev->parent, base_addr + 1, reg);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
/* Control register 3 */
|
|
rc = pm8xxx_readb(dd->dev->parent, base_addr + 2 , ®);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
value = hsed_config->othc_hyst_clk_us /
|
|
hsed_config->othc_hyst_prediv_us;
|
|
if (value > 15) {
|
|
pr_err("Invalid input argument - othc_hyst_prediv_us\n");
|
|
return -EINVAL;
|
|
}
|
|
reg &= PM8058_OTHC_HYST_CLK_MASK;
|
|
reg |= value << PM8058_OTHC_HYST_CLK_SHIFT;
|
|
|
|
value = hsed_config->othc_period_clk_us /
|
|
hsed_config->othc_period_clkdiv_us;
|
|
if (value > 15) {
|
|
pr_err("Invalid input argument - othc_hyst_prediv_us\n");
|
|
return -EINVAL;
|
|
}
|
|
reg = (reg & PM8058_OTHC_PERIOD_CLK_MASK) | value;
|
|
|
|
rc = pm8xxx_writeb(dd->dev->parent, base_addr + 2, reg);
|
|
if (rc < 0) {
|
|
pr_err("PM8058 read failed\n");
|
|
return rc;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t othc_headset_print_name(struct switch_dev *sdev, char *buf)
|
|
{
|
|
switch (switch_get_state(sdev)) {
|
|
case OTHC_NO_DEVICE:
|
|
return sprintf(buf, "No Device\n");
|
|
case OTHC_HEADSET:
|
|
case OTHC_HEADPHONE:
|
|
case OTHC_MICROPHONE:
|
|
case OTHC_ANC_HEADSET:
|
|
case OTHC_ANC_HEADPHONE:
|
|
case OTHC_ANC_MICROPHONE:
|
|
return sprintf(buf, "Headset\n");
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int pm8058_configure_switch(struct pm8058_othc *dd)
|
|
{
|
|
int rc, i;
|
|
|
|
if (dd->othc_support_n_switch == true) {
|
|
/* n-switch support */
|
|
rc = adc_channel_open(dd->switch_config->adc_channel,
|
|
&dd->adc_handle);
|
|
if (rc) {
|
|
pr_err("Unable to open ADC channel\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
for (i = 0; i < dd->switch_config->num_keys; i++) {
|
|
input_set_capability(dd->othc_ipd, EV_KEY,
|
|
dd->switch_config->switch_info[i].key_code);
|
|
}
|
|
} else /* Only single switch supported */
|
|
input_set_capability(dd->othc_ipd, EV_KEY, KEY_MEDIA);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
pm8058_configure_accessory(struct pm8058_othc *dd)
|
|
{
|
|
int i, rc;
|
|
char name[OTHC_GPIO_MAX_LEN];
|
|
|
|
/*
|
|
* Not bailing out if the gpio_* configure calls fail. This is required
|
|
* as multiple accessories are detected by the same gpio.
|
|
*/
|
|
for (i = 0; i < dd->num_accessories; i++) {
|
|
if (dd->accessory_info[i].enabled == false)
|
|
continue;
|
|
if (dd->accessory_info[i].detect_flags & OTHC_GPIO_DETECT) {
|
|
snprintf(name, OTHC_GPIO_MAX_LEN, "%s%d",
|
|
"othc_acc_gpio_", i);
|
|
rc = gpio_request(dd->accessory_info[i].gpio, name);
|
|
if (rc) {
|
|
pr_debug("Unable to request GPIO [%d]\n",
|
|
dd->accessory_info[i].gpio);
|
|
continue;
|
|
}
|
|
rc = gpio_direction_input(dd->accessory_info[i].gpio);
|
|
if (rc) {
|
|
pr_debug("Unable to set-direction GPIO [%d]\n",
|
|
dd->accessory_info[i].gpio);
|
|
gpio_free(dd->accessory_info[i].gpio);
|
|
continue;
|
|
}
|
|
}
|
|
input_set_capability(dd->othc_ipd, EV_SW,
|
|
dd->accessory_info[i].key_code);
|
|
}
|
|
|
|
if (dd->accessories_adc_support) {
|
|
/*
|
|
* Check if 3 switch is supported. If both are using the same
|
|
* ADC channel, the same handle can be used.
|
|
*/
|
|
if (dd->othc_support_n_switch) {
|
|
if (dd->adc_handle != NULL &&
|
|
(dd->accessories_adc_channel ==
|
|
dd->switch_config->adc_channel))
|
|
dd->accessory_adc_handle = dd->adc_handle;
|
|
} else {
|
|
rc = adc_channel_open(dd->accessories_adc_channel,
|
|
&dd->accessory_adc_handle);
|
|
if (rc) {
|
|
pr_err("Unable to open ADC channel\n");
|
|
rc = -ENODEV;
|
|
goto accessory_adc_fail;
|
|
}
|
|
}
|
|
if (dd->video_out_gpio != 0) {
|
|
rc = gpio_request(dd->video_out_gpio, "vout_enable");
|
|
if (rc < 0) {
|
|
pr_err("request VOUT gpio failed (%d)\n", rc);
|
|
goto accessory_adc_fail;
|
|
}
|
|
rc = gpio_direction_output(dd->video_out_gpio, 0);
|
|
if (rc < 0) {
|
|
pr_err("direction_out failed (%d)\n", rc);
|
|
goto accessory_adc_fail;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
accessory_adc_fail:
|
|
for (i = 0; i < dd->num_accessories; i++) {
|
|
if (dd->accessory_info[i].enabled == false)
|
|
continue;
|
|
gpio_free(dd->accessory_info[i].gpio);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int
|
|
othc_configure_hsed(struct pm8058_othc *dd, struct platform_device *pd)
|
|
{
|
|
int rc;
|
|
struct input_dev *ipd;
|
|
struct pmic8058_othc_config_pdata *pdata = pd->dev.platform_data;
|
|
struct othc_hsed_config *hsed_config = pdata->hsed_config;
|
|
|
|
dd->othc_sdev.name = "h2w";
|
|
dd->othc_sdev.print_name = othc_headset_print_name;
|
|
|
|
rc = switch_dev_register(&dd->othc_sdev);
|
|
if (rc) {
|
|
pr_err("Unable to register switch device\n");
|
|
return rc;
|
|
}
|
|
|
|
ipd = input_allocate_device();
|
|
if (ipd == NULL) {
|
|
pr_err("Unable to allocate memory\n");
|
|
rc = -ENOMEM;
|
|
goto fail_input_alloc;
|
|
}
|
|
|
|
/* Get the IRQ for Headset Insert-remove and Switch-press */
|
|
dd->othc_irq_sw = platform_get_irq(pd, 0);
|
|
dd->othc_irq_ir = platform_get_irq(pd, 1);
|
|
if (dd->othc_irq_ir < 0 || dd->othc_irq_sw < 0) {
|
|
pr_err("othc resource:IRQs absent\n");
|
|
rc = -ENXIO;
|
|
goto fail_micbias_config;
|
|
}
|
|
|
|
if (pdata->hsed_name != NULL)
|
|
ipd->name = pdata->hsed_name;
|
|
else
|
|
ipd->name = "pmic8058_othc";
|
|
|
|
ipd->phys = "pmic8058_othc/input0";
|
|
ipd->dev.parent = &pd->dev;
|
|
|
|
dd->othc_ipd = ipd;
|
|
dd->ir_gpio = hsed_config->ir_gpio;
|
|
dd->othc_sw_state = false;
|
|
dd->switch_debounce_ms = hsed_config->switch_debounce_ms;
|
|
dd->othc_support_n_switch = hsed_config->othc_support_n_switch;
|
|
dd->accessory_support = pdata->hsed_config->accessories_support;
|
|
dd->detection_delay_ms = pdata->hsed_config->detection_delay_ms;
|
|
|
|
if (dd->othc_support_n_switch == true)
|
|
dd->switch_config = hsed_config->switch_config;
|
|
|
|
if (dd->accessory_support == true) {
|
|
dd->accessory_info = pdata->hsed_config->accessories;
|
|
dd->num_accessories = pdata->hsed_config->othc_num_accessories;
|
|
dd->accessories_adc_support =
|
|
pdata->hsed_config->accessories_adc_support;
|
|
dd->accessories_adc_channel =
|
|
pdata->hsed_config->accessories_adc_channel;
|
|
dd->video_out_gpio = pdata->hsed_config->video_out_gpio;
|
|
}
|
|
|
|
/* Configure the MIC_BIAS line for headset detection */
|
|
rc = pm8058_configure_micbias(dd);
|
|
if (rc < 0)
|
|
goto fail_micbias_config;
|
|
|
|
/* Configure for the switch events */
|
|
rc = pm8058_configure_switch(dd);
|
|
if (rc < 0)
|
|
goto fail_micbias_config;
|
|
|
|
/* Configure the accessory */
|
|
if (dd->accessory_support == true) {
|
|
rc = pm8058_configure_accessory(dd);
|
|
if (rc < 0)
|
|
goto fail_micbias_config;
|
|
}
|
|
|
|
input_set_drvdata(ipd, dd);
|
|
spin_lock_init(&dd->lock);
|
|
|
|
rc = input_register_device(ipd);
|
|
if (rc) {
|
|
pr_err("Unable to register OTHC device\n");
|
|
goto fail_micbias_config;
|
|
}
|
|
|
|
hrtimer_init(&dd->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
|
|
dd->timer.function = pm8058_othc_timer;
|
|
|
|
/* Request the HEADSET IR interrupt */
|
|
if (dd->ir_gpio < 0) {
|
|
rc = request_threaded_irq(dd->othc_irq_ir, NULL, pm8058_nc_ir,
|
|
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_DISABLED,
|
|
"pm8058_othc_ir", dd);
|
|
if (rc < 0) {
|
|
pr_err("Unable to request pm8058_othc_ir IRQ\n");
|
|
goto fail_ir_irq;
|
|
}
|
|
} else {
|
|
rc = gpio_request(dd->ir_gpio, "othc_ir_gpio");
|
|
if (rc) {
|
|
pr_err("Unable to request IR GPIO\n");
|
|
goto fail_ir_gpio_req;
|
|
}
|
|
rc = gpio_direction_input(dd->ir_gpio);
|
|
if (rc) {
|
|
pr_err("GPIO %d set_direction failed\n", dd->ir_gpio);
|
|
goto fail_ir_irq;
|
|
}
|
|
dd->othc_irq_ir = gpio_to_irq(dd->ir_gpio);
|
|
rc = request_any_context_irq(dd->othc_irq_ir, ir_gpio_irq,
|
|
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
|
|
"othc_gpio_ir_irq", dd);
|
|
if (rc < 0) {
|
|
pr_err("could not request hs irq err=%d\n", rc);
|
|
goto fail_ir_irq;
|
|
}
|
|
}
|
|
/* Request the SWITCH press/release interrupt */
|
|
rc = request_threaded_irq(dd->othc_irq_sw, NULL, pm8058_no_sw,
|
|
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_DISABLED,
|
|
"pm8058_othc_sw", dd);
|
|
if (rc < 0) {
|
|
pr_err("Unable to request pm8058_othc_sw IRQ\n");
|
|
goto fail_sw_irq;
|
|
}
|
|
|
|
/* Check if the accessory is already inserted during boot up */
|
|
if (dd->ir_gpio < 0) {
|
|
rc = pm8xxx_read_irq_stat(dd->dev->parent, dd->othc_irq_ir);
|
|
if (rc < 0) {
|
|
pr_err("Unable to get accessory status at boot\n");
|
|
goto fail_ir_status;
|
|
}
|
|
} else {
|
|
rc = gpio_get_value_cansleep(dd->ir_gpio);
|
|
if (rc < 0) {
|
|
pr_err("Unable to get accessory status at boot\n");
|
|
goto fail_ir_status;
|
|
}
|
|
rc = !rc;
|
|
}
|
|
if (rc) {
|
|
pr_debug("Accessory inserted during boot up\n");
|
|
/* process the data and report the inserted accessory */
|
|
rc = pm8058_accessory_report(dd, 1);
|
|
if (rc)
|
|
pr_debug("Unabele to detect accessory at boot up\n");
|
|
}
|
|
|
|
device_init_wakeup(&pd->dev,
|
|
hsed_config->hsed_bias_config->othc_wakeup);
|
|
|
|
INIT_DELAYED_WORK(&dd->detect_work, detect_work_f);
|
|
|
|
INIT_DELAYED_WORK(&dd->hs_work, hs_worker);
|
|
|
|
if (dd->othc_support_n_switch == true)
|
|
INIT_WORK(&dd->switch_work, switch_work_f);
|
|
|
|
|
|
return 0;
|
|
|
|
fail_ir_status:
|
|
free_irq(dd->othc_irq_sw, dd);
|
|
fail_sw_irq:
|
|
free_irq(dd->othc_irq_ir, dd);
|
|
fail_ir_irq:
|
|
if (dd->ir_gpio != -1)
|
|
gpio_free(dd->ir_gpio);
|
|
fail_ir_gpio_req:
|
|
input_unregister_device(ipd);
|
|
dd->othc_ipd = NULL;
|
|
fail_micbias_config:
|
|
input_free_device(ipd);
|
|
fail_input_alloc:
|
|
switch_dev_unregister(&dd->othc_sdev);
|
|
return rc;
|
|
}
|
|
|
|
static int __devinit pm8058_othc_probe(struct platform_device *pd)
|
|
{
|
|
int rc;
|
|
struct pm8058_othc *dd;
|
|
struct resource *res;
|
|
struct pmic8058_othc_config_pdata *pdata = pd->dev.platform_data;
|
|
|
|
if (pdata == NULL) {
|
|
pr_err("Platform data not present\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
dd = kzalloc(sizeof(*dd), GFP_KERNEL);
|
|
if (dd == NULL) {
|
|
pr_err("Unable to allocate memory\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* Enable runtime PM ops, start in ACTIVE mode */
|
|
rc = pm_runtime_set_active(&pd->dev);
|
|
if (rc < 0)
|
|
dev_dbg(&pd->dev, "unable to set runtime pm state\n");
|
|
pm_runtime_enable(&pd->dev);
|
|
|
|
res = platform_get_resource_byname(pd, IORESOURCE_IO, "othc_base");
|
|
if (res == NULL) {
|
|
pr_err("othc resource:Base address absent\n");
|
|
rc = -ENXIO;
|
|
goto fail_get_res;
|
|
}
|
|
|
|
dd->dev = &pd->dev;
|
|
dd->othc_pdata = pdata;
|
|
dd->othc_base = res->start;
|
|
if (pdata->micbias_regulator == NULL) {
|
|
pr_err("OTHC regulator not specified\n");
|
|
goto fail_get_res;
|
|
}
|
|
|
|
dd->othc_vreg = regulator_get(NULL,
|
|
pdata->micbias_regulator->regulator);
|
|
if (IS_ERR(dd->othc_vreg)) {
|
|
pr_err("regulator get failed\n");
|
|
rc = PTR_ERR(dd->othc_vreg);
|
|
goto fail_get_res;
|
|
}
|
|
|
|
rc = regulator_set_voltage(dd->othc_vreg,
|
|
pdata->micbias_regulator->min_uV,
|
|
pdata->micbias_regulator->max_uV);
|
|
if (rc) {
|
|
pr_err("othc regulator set voltage failed\n");
|
|
goto fail_reg_enable;
|
|
}
|
|
|
|
rc = regulator_enable(dd->othc_vreg);
|
|
if (rc) {
|
|
pr_err("othc regulator enable failed\n");
|
|
goto fail_reg_enable;
|
|
}
|
|
|
|
platform_set_drvdata(pd, dd);
|
|
|
|
if (pdata->micbias_capability == OTHC_MICBIAS_HSED) {
|
|
/* HSED to be supported on this MICBIAS line */
|
|
if (pdata->hsed_config != NULL) {
|
|
rc = othc_configure_hsed(dd, pd);
|
|
if (rc < 0)
|
|
goto fail_othc_hsed;
|
|
} else {
|
|
pr_err("HSED config data not present\n");
|
|
rc = -EINVAL;
|
|
goto fail_othc_hsed;
|
|
}
|
|
}
|
|
|
|
/* Store the local driver data structure */
|
|
if (dd->othc_pdata->micbias_select < OTHC_MICBIAS_MAX)
|
|
config[dd->othc_pdata->micbias_select] = dd;
|
|
|
|
pr_debug("Device %s:%d successfully registered\n",
|
|
pd->name, pd->id);
|
|
return 0;
|
|
|
|
fail_othc_hsed:
|
|
regulator_disable(dd->othc_vreg);
|
|
fail_reg_enable:
|
|
regulator_put(dd->othc_vreg);
|
|
fail_get_res:
|
|
pm_runtime_set_suspended(&pd->dev);
|
|
pm_runtime_disable(&pd->dev);
|
|
|
|
kfree(dd);
|
|
return rc;
|
|
}
|
|
|
|
static struct platform_driver pm8058_othc_driver = {
|
|
.driver = {
|
|
.name = "pm8058-othc",
|
|
.owner = THIS_MODULE,
|
|
#ifdef CONFIG_PM
|
|
.pm = &pm8058_othc_pm_ops,
|
|
#endif
|
|
},
|
|
.probe = pm8058_othc_probe,
|
|
.remove = __devexit_p(pm8058_othc_remove),
|
|
};
|
|
|
|
static int __init pm8058_othc_init(void)
|
|
{
|
|
return platform_driver_register(&pm8058_othc_driver);
|
|
}
|
|
|
|
static void __exit pm8058_othc_exit(void)
|
|
{
|
|
platform_driver_unregister(&pm8058_othc_driver);
|
|
}
|
|
/*
|
|
* Move to late_initcall, to make sure that the ADC driver registration is
|
|
* completed before we open a ADC channel.
|
|
*/
|
|
late_initcall(pm8058_othc_init);
|
|
module_exit(pm8058_othc_exit);
|
|
|
|
MODULE_ALIAS("platform:pmic8058_othc");
|
|
MODULE_DESCRIPTION("PMIC 8058 OTHC");
|
|
MODULE_LICENSE("GPL v2");
|