diff options
author | NeilBrown <neilb@suse.de> | 2013-07-06 09:46:21 +1000 |
---|---|---|
committer | NeilBrown <neilb@suse.de> | 2013-07-09 11:36:01 +1000 |
commit | 9b0aaec962e0ac8b8ea8fb6434eea4ced65231c5 (patch) | |
tree | 73d805ee0221e505e76ffb747dae5506cc516954 /sound/soc/omap | |
parent | c8349b3bf47a0f521c740e7397bd2cad8dac5d96 (diff) | |
download | kernel_goldelico_gta04-9b0aaec962e0ac8b8ea8fb6434eea4ced65231c5.zip kernel_goldelico_gta04-9b0aaec962e0ac8b8ea8fb6434eea4ced65231c5.tar.gz kernel_goldelico_gta04-9b0aaec962e0ac8b8ea8fb6434eea4ced65231c5.tar.bz2 |
GTA04 audio jack support.
Separate "driver" linked in to the twl4030 audio driver.
Auto and manual calibration.
Signed-off-by: NeilBrown <neilb@suse.de>
Diffstat (limited to 'sound/soc/omap')
-rw-r--r-- | sound/soc/omap/Makefile | 2 | ||||
-rw-r--r-- | sound/soc/omap/gta04-jack.c | 230 | ||||
-rw-r--r-- | sound/soc/omap/omap-twl4030.c | 7 |
3 files changed, 238 insertions, 1 deletions
diff --git a/sound/soc/omap/Makefile b/sound/soc/omap/Makefile index 128b509..09cd9e2 100644 --- a/sound/soc/omap/Makefile +++ b/sound/soc/omap/Makefile @@ -20,7 +20,7 @@ snd-soc-am3517evm-objs := am3517evm.o snd-soc-omap-abe-twl6040-objs := omap-abe-twl6040.o snd-soc-omap-twl4030-objs := omap-twl4030.o snd-soc-omap3pandora-objs := omap3pandora.o -snd-soc-gta04-objs := gta04-voice.o gta04-headset.o +snd-soc-gta04-objs := gta04-voice.o gta04-headset.o gta04-jack.o # gta04-fm.o gta04-audio.o snd-soc-omap-hdmi-card-objs := omap-hdmi-card.o diff --git a/sound/soc/omap/gta04-jack.c b/sound/soc/omap/gta04-jack.c new file mode 100644 index 0000000..0d96582 --- /dev/null +++ b/sound/soc/omap/gta04-jack.c @@ -0,0 +1,230 @@ + +/* + * Jack driver for GTA04. + * Copyright Neil Brown <neilb@suse.de> 2013 + * + * The DC current through the headset microphone pins is + * converted to a voltage which is presented on TWL4030 madc 7. + * To be able to read a current, the Headset Mic Bias must + * be enabled. + * + * When the jack device is open, enable the Headset Mic Bias + * and poll mdac 7 every 500msec. Once we see an insertion, + * we increase the rate to ever 50msec until we see a removal. + * + * There are 4 possible states: + * - Nothing plugged in, open circuit - voltage is low + * - short circuit due to headphone with no mic (3-contact TRS) + * inserted. Voltage is high. + * - short circuit due to button on headset being pushed. + * Voltage is also high. + * - Microphone is in circuit. Voltage is even higher. I don't + * understand how it can be higher than than with a short + * circuit, but that is what I measure. + * + * To differentiate between the two short circuits we look at how + * we got there. A transition from open to short means a 3-contact + * TRS with no mic. A transition from mic to short means the button + * on the mic was pressed. + * + * As different devices report different actual voltages we need + * some calibration. As we cannot do this automatically we complete + * precision, we allow user-space to tell us the calibration. + * We assume that open-circuit is always below 100, and other + * readings are above that. + * The highest level we see for 3 consecutive readings is assumed + * to be the 'microphone' level, and short-circuit is 5% below that. + * If headphones with no mic are inserted this will be wrong, but not + * terribly wrong. As soon as a headset with a mic is inserted it + * will get corrected and stay corrected. + * In order to keep this correct across a reboot, user-space can + * read the current setting from + * /sys/modules/snd_soc_gta04/parameters/jack_level. + * and then write back the value after reboot. Once a value is + * written, auto-calibration is disabled. + * Writing the value '0' can re-enable auto-calibration. + */ + +#include <linux/input.h> +#include <sound/jack.h> +#include <sound/soc.h> +#include <linux/suspend.h> +#include <linux/i2c/twl4030-madc.h> +#include <linux/module.h> + +static struct { + struct snd_soc_jack hs_jack; + struct delayed_work jack_work; + struct snd_soc_codec *codec; + int open; + /* When any jack is present, we: + * - poll more quickly to catch button presses + * - assume a 'short' is 'button press', not 'headset has + * no mic + * 'present' stores SND_JACK_HEADPHONE or SND_JACK_HEADSET + * indicating what we think is present. + */ + int present; + /* Calibration reports a single number which roughly + * points to 'short'. + * Less than half this is 'open circuit'. + * More than this is 'microphone + */ + long level; + int level_fixed; + long level_new; + int level_count; +} jack; + +static void gta04_jack_work(struct work_struct *work) +{ + long val; + long delay = msecs_to_jiffies(500); + int jackbits; + + /* choose delay *before* checking presence so we still get + * one long delay on first insertion to help with debounce. + */ + if (jack.present) + delay = msecs_to_jiffies(50); + + val = twl4030_get_madc_conversion(7); + if (val < 0) + goto out; + /* On my device: + * open circuit = around 20 + * short circuit = around 800, or 325 on another device + * microphone = around 830-840 !!! 345 on other device. + */ + if (!jack.level_fixed) { + if (jack.level * 21/20 + 2 < val) { + if (jack.level_count == 0 || + val < jack.level_new*21/20) + jack.level_new = val*20/21; + if (jack.level_count >= 3) { + jack.level = jack.level_new; + jack.level_count = 0; + } else + jack.level_count += 1; + } else + jack.level_count = 0; + } + if (val < jack.level / 2) { + /* open circuit */ + jackbits = 0; + jack.present = 0; + /* debounce */ + delay = msecs_to_jiffies(500); + } else if (val < jack.level) { + /* short */ + if (jack.present == 0) { + /* Inserted headset with no mic */ + jack.present = SND_JACK_HEADPHONE; + jackbits = jack.present; + } else if (jack.present & SND_JACK_MICROPHONE) { + /* mic shorted -> button press */ + jackbits = SND_JACK_BTN_0 | jack.present; + } else { + /* headphones still present */ + jackbits = jack.present; + } + } else { + /* There is a microphone there */ + jack.present = SND_JACK_HEADSET; + jackbits = jack.present; + } + snd_soc_jack_report(&jack.hs_jack, jackbits, + SND_JACK_HEADSET | SND_JACK_BTN_0); + +out: + if (jack.open) + schedule_delayed_work(&jack.jack_work, delay); +} + +static int gta04_jack_pm_notify(struct notifier_block *b, unsigned long v, void *d) +{ + if (!jack.codec || !jack.open) + return 0; + switch(v) { + case PM_SUSPEND_PREPARE: + /* Disable Headset Mic Bias while asleep */ + snd_soc_dapm_disable_pin(&jack.codec->dapm, "Headset Mic Bias"); + snd_soc_dapm_sync(&jack.codec->dapm); + break; + + case PM_POST_SUSPEND: + snd_soc_dapm_force_enable_pin(&jack.codec->dapm, "Headset Mic Bias"); + snd_soc_dapm_sync(&jack.codec->dapm); + break; + default: break; + } + return 0; +} + +static struct notifier_block gta04_jack_pm_notify_block = { + .notifier_call = gta04_jack_pm_notify, +}; + +static int gta04_jack_open(struct input_dev *dev) +{ + snd_soc_dapm_force_enable_pin(&jack.codec->dapm, "Headset Mic Bias"); + snd_soc_dapm_sync(&jack.codec->dapm); + jack.open = 1; + schedule_delayed_work(&jack.jack_work, msecs_to_jiffies(100)); + return 0; +} + +static void gta04_jack_close(struct input_dev *dev) +{ + jack.open = 0; + cancel_delayed_work_sync(&jack.jack_work); + snd_soc_dapm_disable_pin(&jack.codec->dapm, "Headset Mic Bias"); + snd_soc_dapm_sync(&jack.codec->dapm); +} + +int gta04_jack_probe(struct snd_soc_codec *codec) +{ + struct snd_soc_dapm_context *dapm = &codec->dapm; + int ret; + ret = snd_soc_jack_new(codec, "Headset Jack", + SND_JACK_HEADSET | SND_JACK_BTN_0, + &jack.hs_jack); + if (ret) + return ret; + register_pm_notifier(>a04_jack_pm_notify_block); + + INIT_DELAYED_WORK(&jack.jack_work, gta04_jack_work); + jack.codec = codec; + if (jack.level < 100) + jack.level = 100; + jack.hs_jack.jack->input_dev->open = gta04_jack_open; + jack.hs_jack.jack->input_dev->close = gta04_jack_close; + + return snd_soc_dapm_sync(dapm); +} + +void gta04_jack_remove(struct snd_soc_codec *codec) +{ + unregister_pm_notifier(>a04_jack_pm_notify_block); + cancel_delayed_work(&jack.jack_work); +} + +static int get_level(char *buffer, struct kernel_param *kp) +{ + return sprintf(buffer, "%ld", jack.level); +} +static int set_level(const char *val, struct kernel_param *kp) +{ + long num; + if (kstrtol(val, 10, &num) < 0) + return -EINVAL; + if (num == 0) { + jack.level = 100; + jack.level_fixed = 0; + } else { + jack.level = num; + jack.level_fixed = 1; + } + return 0; +} +module_param_call(jack_level, set_level, get_level, NULL, S_IRUSR|S_IWUSR); diff --git a/sound/soc/omap/omap-twl4030.c b/sound/soc/omap/omap-twl4030.c index 2a9324f..3824f15 100644 --- a/sound/soc/omap/omap-twl4030.c +++ b/sound/soc/omap/omap-twl4030.c @@ -47,6 +47,7 @@ struct omap_twl4030 { int jack_detect; /* board can detect jack events */ struct snd_soc_jack hs_jack; + void (*jack_remove)(struct snd_soc_codec *codec); }; static int omap_twl4030_hw_params(struct snd_pcm_substream *substream, @@ -229,6 +230,10 @@ static int omap_twl4030_init(struct snd_soc_pcm_runtime *rtd) twl4030_disconnect_pin(dapm, pdata->has_digimic1, "Digital1 Mic"); twl4030_disconnect_pin(dapm, pdata->has_linein, "Line In"); + if (pdata->jack_init && + pdata->jack_init(codec)) + priv->jack_remove = pdata->jack_remove; + return ret; } @@ -357,6 +362,8 @@ static int omap_twl4030_remove(struct platform_device *pdev) snd_soc_jack_free_gpios(&priv->hs_jack, ARRAY_SIZE(hs_jack_gpios), hs_jack_gpios); + if (priv->jack_remove) + priv->jack_remove(NULL); snd_soc_unregister_card(card); return 0; |