aboutsummaryrefslogtreecommitdiffstats
path: root/sound/soc/omap
diff options
context:
space:
mode:
Diffstat (limited to 'sound/soc/omap')
-rw-r--r--sound/soc/omap/Makefile2
-rw-r--r--sound/soc/omap/gta04-jack.c230
-rw-r--r--sound/soc/omap/omap-twl4030.c7
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(&gta04_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(&gta04_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;