Re: [RFC v1 PATCH 5/6] input: pmic8058-othc: Add support for PM8058based OTHC

From: Anirudh Ghayal
Date: Tue Dec 07 2010 - 07:11:14 EST


Hi Dimitry,

Thanks for your comments.

On 12/7/2010 3:34 PM, Dmitry Torokhov wrote:
Hi Trilok,

On Wed, Nov 10, 2010 at 06:18:00PM +0530, Trilok Soni wrote:
From: Anirudh Ghayal<aghayal@xxxxxxxxxxxxxx>

One-touch headset controller is a hardware module in Qualcomm's PMIC8058.
It supports headset insert/remove and switch press/release detection events
over 3 MIC BIAS lines. The MIC BIAS lines can be configured to support
headset detection or act as regular BIAS lines.

Cc: Dmitry Torokhov<dmitry.torokhov@xxxxxxxxx>
Signed-off-by: Anirudh Ghayal<aghayal@xxxxxxxxxxxxxx>
---
drivers/input/misc/Kconfig | 10 +
drivers/input/misc/Makefile | 1 +
drivers/input/misc/pmic8058-othc.c | 544 +++++++++++++++++++++++++++++++++++
include/linux/input/pmic8058-othc.h | 117 ++++++++
4 files changed, 672 insertions(+), 0 deletions(-)
create mode 100644 drivers/input/misc/pmic8058-othc.c
create mode 100644 include/linux/input/pmic8058-othc.h

diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index aeb9165..df6097c 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -359,6 +359,16 @@ config INPUT_PMIC8058_PWRKEY
To compile this driver as a module, choose M here: the
module will be called pmic8058-pwrkey.

+config INPUT_PMIC8058_OTHC
+ tristate "Qualcomm PMIC8058 OTHC support"
+ depends on PMIC8058
+ help
+ Say Y here if you want support PMIC8058 One-touch Headset Controller
+ (OTHC)
+
+ To compile this driver as a module, choose M here: the
+ module will be called pmic8058-othc.
+
config INPUT_GPIO_ROTARY_ENCODER
tristate "Rotary encoders connected to GPIO pins"
depends on GPIOLIB&& GENERIC_GPIO
diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
index c4357a0..a713370 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -29,6 +29,7 @@ obj-$(CONFIG_INPUT_PCAP) += pcap_keys.o
obj-$(CONFIG_INPUT_PCF50633_PMU) += pcf50633-input.o
obj-$(CONFIG_INPUT_PCF8574) += pcf8574_keypad.o
obj-$(CONFIG_INPUT_PCSPKR) += pcspkr.o
+obj-$(CONFIG_INPUT_PMIC8058_OTHC) += pmic8058-othc.o
obj-$(CONFIG_INPUT_POWERMATE) += powermate.o
obj-$(CONFIG_INPUT_PWM_BEEPER) += pwm-beeper.o
obj-$(CONFIG_INPUT_PMIC8058_PWRKEY) += pmic8058-pwrkey.o
diff --git a/drivers/input/misc/pmic8058-othc.c b/drivers/input/misc/pmic8058-othc.c
new file mode 100644
index 0000000..78f157a
--- /dev/null
+++ b/drivers/input/misc/pmic8058-othc.c
@@ -0,0 +1,544 @@
+/* Copyright (c) 2010, Code Aurora Forum. 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#define pr_fmt(fmt) "%s:" fmt, __func__
+
+#include<linux/module.h>
+#include<linux/init.h>
+#include<linux/kernel.h>
+#include<linux/slab.h>
+#include<linux/interrupt.h>
+#include<linux/input.h>
+#include<linux/platform_device.h>
+#include<linux/pm.h>
+
+#include<linux/mfd/pmic8058.h>
+#include<linux/input/pmic8058-othc.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 PM8058_OTHC_LOW_CURR_MIRROR 10
+#define PM8058_OTHC_HIGH_CURR_MIRROR 100
+#define PM8058_OTHC_CLK_SRC_SHIFT 10
+
+/**
+ * struct pm8058_othc - othc driver data structure
+ * @othc_ipd: input device for othc
+ * @othc_pdata: a pointer to the platform data
+ * @othc_base: base address of the OTHC controller
+ * @othc_irq_sw: switch detect irq number
+ * @othc_irq_ir: headset jack detect irq number
+ * @othc_sw_state: current state of the switch
+ * @othc_ir_state: current state of the headset
+ * @switch_reject: indicates if valid switch press
+ * @switch_debounce_ms: the debounce time for the switch
+ * @lock: spin lock variable
+ * @timer: timer for switch debounce time
+ * @pm_chip: pointer to the pm8058 parent chip
+ */
+struct pm8058_othc {
+ struct input_dev *othc_ipd;
+ struct pmic8058_othc_config_pdata *othc_pdata;

const?

Ok.


+ int othc_base;
+ int othc_irq_sw;
+ int othc_irq_ir;
+ bool othc_sw_state;
+ bool othc_ir_state;
+ bool switch_reject;
+ unsigned long switch_debounce_ms;
+ spinlock_t lock;
+ struct timer_list timer;
+ struct pm8058_chip *pm_chip;
+};
+
+static struct pm8058_othc *config[OTHC_MICBIAS_MAX];
+
+/**
+ * pm8058_micbias_enable() - Enables/Disables the MIC_BIAS
+ * @micbias: MIC BIAS line which needs to be enabled
+ * @enable: operational state for the MIC BIAS line
+ *
+ * The API pm8058_micbias_enable() configures 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, 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 = pm8058_read(dd->pm_chip, dd->othc_base + 1,&reg, 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 = pm8058_write(dd->pm_chip, dd->othc_base + 1,&reg, 1);
+ if (rc< 0) {
+ pr_err("PM8058 write failed\n");
+ return rc;
+ }
+
+ return rc;
+}
+EXPORT_SYMBOL(pm8058_micbias_enable);
+
+#ifdef CONFIG_PM
+static int pm8058_othc_suspend(struct device *dev)
+{
+ 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);
+ }
+ }
+
+ return 0;
+}
+
+static int pm8058_othc_resume(struct device *dev)
+{
+ 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);
+ }
+ }
+
+ return 0;
+}
+
+static const 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);
+
+ if (dd->othc_pdata->micbias_capability == OTHC_MICBIAS_HSED) {

Why do we even bind to devices that are not OTHC_MICBIAS_HSED?

On PMIC 8058 we have 3 MIC_BIAS lines and all 3 have the capability to support HSED OTHC controller. So, I register all the 3 BIAS lines, though only the ones which are enabled for HSED regitser as input devices. Also, I export an API pm8058_micbias_enable (above) which allows to configure the non-HSED lines as regualr MIC_BIAS lines.


+ device_init_wakeup(&pd->dev, 0);
+ free_irq(dd->othc_irq_sw, dd);
+ free_irq(dd->othc_irq_ir, dd);
+ del_timer_sync(&dd->timer);
+ input_unregister_device(dd->othc_ipd);
+ }
+
+ kfree(dd);
+
+ return 0;
+}
+
+static void pm8058_othc_timer(unsigned long handle)
+{
+ unsigned long flags;
+ struct pm8058_othc *dd = (struct pm8058_othc *)handle;
+
+ spin_lock_irqsave(&dd->lock, flags);
+ dd->switch_reject = false;
+ spin_unlock_irqrestore(&dd->lock, flags);
+}
+
+static irqreturn_t pm8058_no_sw(int irq, void *dev_id)
+{
+ struct pm8058_othc *dd = dev_id;
+ unsigned long flags;
+
+ /*
+ * Due to a hardware bug, spurious switch interrutps are seen while
+ * inserting the headset slowly. A timer based logic rejects these
+ * switch interrutps.
+ */
+ spin_lock_irqsave(&dd->lock, flags);
+ if (dd->switch_reject == true) {
+ spin_unlock_irqrestore(&dd->lock, flags);
+ return IRQ_HANDLED;
+ }
+ spin_unlock_irqrestore(&dd->lock, flags);

I am not quite sure whether this locking helps anything... You do
protect the check by condition can change once you leave the protected
section since both IRQ handles and timer can run simultaneously...

Sorry, I didn't quite get your concern. Here, only the timer updates the varible and the int. handler checks for the condition and returns. As these both can occur in parallel we need a lock.


+
+ if (dd->othc_sw_state == false) {
+ dd->othc_sw_state = true;
+ input_report_key(dd->othc_ipd, KEY_MEDIA, 1);
+ } else if (dd->othc_sw_state == true) {
+ dd->othc_sw_state = false;
+ input_report_key(dd->othc_ipd, KEY_MEDIA, 0);
+ }
+ input_sync(dd->othc_ipd);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t pm8058_nc_ir(int irq, void *dev_id)
+{
+ unsigned long flags;
+ struct pm8058_othc *dd = dev_id;
+
+ spin_lock_irqsave(&dd->lock, flags);
+ dd->switch_reject = true;
+ spin_unlock_irqrestore(&dd->lock, flags);
+
+ mod_timer(&dd->timer, jiffies +
+ msecs_to_jiffies(dd->switch_debounce_ms));
+
+ if (dd->othc_ir_state == false) {
+ dd->othc_ir_state = true;
+ input_report_key(dd->othc_ipd, SW_HEADPHONE_INSERT, 1);
+ } else {
+ dd->othc_ir_state = false;
+ input_report_key(dd->othc_ipd, SW_HEADPHONE_INSERT, 0);
+ }
+
+ input_sync(dd->othc_ipd);

dd->othc_ir_state = !dd->othc_ir_state;
input_report_key(dd->othc_ipd, SW_HEADPHONE_INSERT, dd->othc_ir_state);
input_sync(dd->othc_ipd);

Simpler :).


+
+ return IRQ_HANDLED;
+}
+
+static int pm8058_configure_othc(struct pm8058_othc *dd)
+{
+ int rc;
+ u8 reg, value;
+ u32 value1;
+ u16 base_addr = dd->othc_base;
+ struct othc_hsed_config *hsed_config = dd->othc_pdata->hsed_config;
+
+ /* Control Register 1*/
+ rc = pm8058_read(dd->pm_chip, base_addr,&reg, 1);
+ if (rc< 0) {
+ pr_err("PM8058 read failed\n");
+ return rc;
+ }
+
+ if (hsed_config->othc_headset == OTHC_HEADSET_NO) {
+ /* set iDAC high current threshold */
+ value = (hsed_config->othc_highcurr_thresh_uA /
+ PM8058_OTHC_HIGH_CURR_MIRROR) - 2;
+ reg = (reg& PM8058_OTHC_HIGH_CURR_MASK) | value;
+ } else {
+ /* set iDAC low current threshold */
+ value = (hsed_config->othc_lowcurr_thresh_uA /
+ PM8058_OTHC_LOW_CURR_MIRROR) - 1;
+ reg&= PM8058_OTHC_LOW_CURR_MASK;
+ reg |= (value<< PM8058_OTHC_LOW_CURR_SHIFT);
+ }
+
+ rc = pm8058_write(dd->pm_chip, base_addr,&reg, 1);
+ if (rc< 0) {
+ pr_err("PM8058 read failed\n");
+ return rc;
+ }
+
+ /* Control register 2*/
+ rc = pm8058_read(dd->pm_chip, base_addr + 1,&reg, 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<<
+ PM8058_OTHC_CLK_SRC_SHIFT) / 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<<
+ PM8058_OTHC_CLK_SRC_SHIFT) / 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 = pm8058_write(dd->pm_chip, base_addr + 1,&reg, 1);
+ if (rc< 0) {
+ pr_err("PM8058 read failed\n");
+ return rc;
+ }
+
+ /* Control register 3 */
+ rc = pm8058_read(dd->pm_chip, base_addr + 2 ,&reg, 1);
+ 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 = pm8058_write(dd->pm_chip, base_addr + 2,&reg, 1);
+ if (rc< 0) {
+ pr_err("PM8058 read failed\n");
+ return rc;
+ }
+
+ return 0;
+}
+
+static int
+pm8058_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;

const?

+ struct othc_hsed_config *hsed_config = pdata->hsed_config;

And here as well?


Sure.

+
+ ipd = input_allocate_device();
+ if (ipd == NULL) {
+ dev_err(&pd->dev, "Memory allocate to input device failed!\n");
+ rc = -ENOMEM;
+ goto fail_input_alloc;
+ }
+
+ 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) {
+ dev_err(&pd->dev, "othc resource:IRQ_IR absent!\n");
+ rc = -ENXIO;
+ goto fail_othc_config;
+ }
+
+ ipd->name = "pmic8058_othc";
+ ipd->phys = "pmic8058_othc/input0";
+ ipd->dev.parent =&pd->dev;
+
+ input_set_capability(ipd, EV_SW, SW_HEADPHONE_INSERT);
+ input_set_capability(ipd, EV_KEY, KEY_MEDIA);

What exactly this button is supposed to do? I do not think KEY_MEDIA is
the one you need here.

This key acts as send/end key while in call and play/pause in case of audio playback. So we used KEY_MEDIA as a generic media key. Could you please suggest what can be used here?



+
+ input_set_drvdata(ipd, dd);
+
+ dd->othc_ipd = ipd;
+ dd->othc_sw_state = false;
+ dd->othc_ir_state = false;
+ spin_lock_init(&dd->lock);
+ dd->switch_debounce_ms = hsed_config->switch_debounce_ms;
+ setup_timer(&dd->timer, pm8058_othc_timer, (unsigned long) dd);
+
+ rc = pm8058_configure_othc(dd);
+ if (rc< 0)
+ goto fail_othc_config;
+
+ rc = input_register_device(ipd);
+ if (rc) {
+ dev_err(&pd->dev, "Register OTHC device failed!\n");
+ goto fail_othc_config;
+ }
+
+ /* Check if the headset is already inserted during boot up */
+ rc = pm8058_irq_get_rt_status(dd->pm_chip, dd->othc_irq_ir);
+ if (rc< 0) {
+ dev_err(&pd->dev, "Unable to get headset status at boot!\n");
+ goto fail_ir_irq;
+ }
+ if (rc) {
+ dev_dbg(&pd->dev, "Headset inserted during boot up!\n");
+ dd->othc_ir_state = true;
+ input_report_switch(dd->othc_ipd, SW_HEADPHONE_INSERT, 1);
+ input_sync(dd->othc_ipd);
+ }
+
+ rc = request_any_context_irq(dd->othc_irq_ir, pm8058_nc_ir,
+ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ "pm8058_othc_ir", dd);

Hmm, non-threaded IRQs do not support IRQF_ONESHOT, do they? BTW, is
oneshot really needed here?

Right, I donot need IRQF_ONESHOT here.


+ if (rc< 0) {
+ dev_err(&pd->dev, "Request pm8058_othc_ir IRQ failed!\n");
+ goto fail_ir_irq;
+ }
+
+ rc = request_any_context_irq(dd->othc_irq_sw, pm8058_no_sw,
+ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ "pm8058_othc_sw", dd);
+ if (rc< 0) {
+ dev_err(&pd->dev, "Request pm8058_othc_sw IRQ failed!\n");
+ goto fail_sw_irq;
+ }
+
+ device_init_wakeup(&pd->dev, hsed_config->othc_wakeup);
+
+ return 0;
+
+fail_sw_irq:
+ free_irq(dd->othc_irq_ir, dd);
+fail_ir_irq:
+ input_unregister_device(ipd);
+ dd->othc_ipd = NULL;
+fail_othc_config:
+ input_free_device(ipd);
+fail_input_alloc:
+ return rc;
+}
+
+static int __devinit pm8058_othc_probe(struct platform_device *pd)
+{
+ int rc;
+ struct pm8058_othc *dd;
+ struct pm8058_chip *chip;
+ struct resource *res;
+ struct pmic8058_othc_config_pdata *pdata = pd->dev.platform_data;
+
+ chip = platform_get_drvdata(pd);
+ if (chip == NULL) {
+ dev_err(&pd->dev, "Invalid driver information!\n");
+ return -EINVAL;
+ }
+
+ /* PMIC8058 version A0 not supported */
+ if (pm8058_rev(chip) == PM_8058_REV_1p0) {
+ dev_err(&pd->dev, "PMIC8058 version not supported!\n");
+ return -ENODEV;
+ }
+
+ if (pdata == NULL) {
+ dev_err(&pd->dev, "Platform data not present!\n");
+ return -EINVAL;
+ }
+
+ dd = kzalloc(sizeof(*dd), GFP_KERNEL);
+ if (dd == NULL) {
+ dev_err(&pd->dev, "Unable to allocate memory!\n");
+ return -ENOMEM;
+ }
+
+ res = platform_get_resource_byname(pd, IORESOURCE_IO, "othc_base");
+ if (res == NULL) {
+ dev_err(&pd->dev, "OTHC Base address, resource absent!\n");
+ rc = -ENXIO;
+ goto fail_get_res;
+ }
+
+ dd->othc_pdata = pdata;
+ dd->pm_chip = chip;
+ dd->othc_base = res->start;
+
+ 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 = pm8058_othc_configure_hsed(dd, pd);
+ if (rc< 0)
+ goto fail_get_res;
+ } else {
+ dev_err(&pd->dev, "HSED config data absent!\n");
+ rc = -EINVAL;
+ goto fail_get_res;
+ }
+ }
+
+ /* Store the local driver data structure */
+ if (dd->othc_pdata->micbias_select< OTHC_MICBIAS_MAX)
+ config[dd->othc_pdata->micbias_select] = dd;
+
+ dev_dbg(&pd->dev, "Device %s:%d successfully registered\n",
+ pd->name, pd->id);
+ return 0;
+
+fail_get_res:
+ kfree(dd);
+ return rc;
+}
+

Thanks.



--
Sent by a consultant of the Qualcomm Innovation Center, Inc.
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum.
--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/