Commit 913d7b4c authored by Mark Brown's avatar Mark Brown

ASoC: Add support for WM8960 capless mode

The WM8960 headphone outputs can be run in capless mode with OUT3
used to drive a pseudo ground for the headphone drivers. In this
mode the mono mixer is not used, the mixer should be turned on
in concert with the headphone output drivers and the device bias
levels are managed differently.

Also tweak the existing bias management to remove the use of active
discharge while we're at it since that's often audible.
Signed-off-by: default avatarMark Brown <broonie@opensource.wolfsonmicro.com>
Acked-by: default avatarLiam Girdwood <lrg@slimlogic.co.uk>
parent b6877a47
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
#define WM8960_DRES_MAX 3 #define WM8960_DRES_MAX 3
struct wm8960_data { struct wm8960_data {
bool capless; /* Headphone outputs configured in capless mode */
int dres; /* Discharge resistance for headphone outputs */ int dres; /* Discharge resistance for headphone outputs */
}; };
......
...@@ -31,8 +31,14 @@ ...@@ -31,8 +31,14 @@
struct snd_soc_codec_device soc_codec_dev_wm8960; struct snd_soc_codec_device soc_codec_dev_wm8960;
/* R25 - Power 1 */ /* R25 - Power 1 */
#define WM8960_VMID_MASK 0x180
#define WM8960_VREF 0x40 #define WM8960_VREF 0x40
/* R26 - Power 2 */
#define WM8960_PWR2_LOUT1 0x40
#define WM8960_PWR2_ROUT1 0x20
#define WM8960_PWR2_OUT3 0x02
/* R28 - Anti-pop 1 */ /* R28 - Anti-pop 1 */
#define WM8960_POBCTRL 0x80 #define WM8960_POBCTRL 0x80
#define WM8960_BUFDCOPEN 0x10 #define WM8960_BUFDCOPEN 0x10
...@@ -42,6 +48,7 @@ struct snd_soc_codec_device soc_codec_dev_wm8960; ...@@ -42,6 +48,7 @@ struct snd_soc_codec_device soc_codec_dev_wm8960;
/* R29 - Anti-pop 2 */ /* R29 - Anti-pop 2 */
#define WM8960_DISOP 0x40 #define WM8960_DISOP 0x40
#define WM8960_DRES_MASK 0x30
/* /*
* wm8960 register cache * wm8960 register cache
...@@ -68,6 +75,9 @@ static const u16 wm8960_reg[WM8960_CACHEREGNUM] = { ...@@ -68,6 +75,9 @@ static const u16 wm8960_reg[WM8960_CACHEREGNUM] = {
struct wm8960_priv { struct wm8960_priv {
u16 reg_cache[WM8960_CACHEREGNUM]; u16 reg_cache[WM8960_CACHEREGNUM];
struct snd_soc_codec codec; struct snd_soc_codec codec;
struct snd_soc_dapm_widget *lout1;
struct snd_soc_dapm_widget *rout1;
struct snd_soc_dapm_widget *out3;
}; };
#define wm8960_reset(c) snd_soc_write(c, WM8960_RESET, 0) #define wm8960_reset(c) snd_soc_write(c, WM8960_RESET, 0)
...@@ -226,10 +236,6 @@ SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_POWER3, 2, 0, ...@@ -226,10 +236,6 @@ SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_POWER3, 2, 0,
&wm8960_routput_mixer[0], &wm8960_routput_mixer[0],
ARRAY_SIZE(wm8960_routput_mixer)), ARRAY_SIZE(wm8960_routput_mixer)),
SND_SOC_DAPM_MIXER("Mono Output Mixer", WM8960_POWER2, 1, 0,
&wm8960_mono_out[0],
ARRAY_SIZE(wm8960_mono_out)),
SND_SOC_DAPM_PGA("LOUT1 PGA", WM8960_POWER2, 6, 0, NULL, 0), SND_SOC_DAPM_PGA("LOUT1 PGA", WM8960_POWER2, 6, 0, NULL, 0),
SND_SOC_DAPM_PGA("ROUT1 PGA", WM8960_POWER2, 5, 0, NULL, 0), SND_SOC_DAPM_PGA("ROUT1 PGA", WM8960_POWER2, 5, 0, NULL, 0),
...@@ -248,6 +254,17 @@ SND_SOC_DAPM_OUTPUT("SPK_RN"), ...@@ -248,6 +254,17 @@ SND_SOC_DAPM_OUTPUT("SPK_RN"),
SND_SOC_DAPM_OUTPUT("OUT3"), SND_SOC_DAPM_OUTPUT("OUT3"),
}; };
static const struct snd_soc_dapm_widget wm8960_dapm_widgets_out3[] = {
SND_SOC_DAPM_MIXER("Mono Output Mixer", WM8960_POWER2, 1, 0,
&wm8960_mono_out[0],
ARRAY_SIZE(wm8960_mono_out)),
};
/* Represent OUT3 as a PGA so that it gets turned on with LOUT1/ROUT1 */
static const struct snd_soc_dapm_widget wm8960_dapm_widgets_capless[] = {
SND_SOC_DAPM_PGA("OUT3 VMID", WM8960_POWER2, 1, 0, NULL, 0),
};
static const struct snd_soc_dapm_route audio_paths[] = { static const struct snd_soc_dapm_route audio_paths[] = {
{ "Left Boost Mixer", "LINPUT1 Switch", "LINPUT1" }, { "Left Boost Mixer", "LINPUT1 Switch", "LINPUT1" },
{ "Left Boost Mixer", "LINPUT2 Switch", "LINPUT2" }, { "Left Boost Mixer", "LINPUT2 Switch", "LINPUT2" },
...@@ -278,9 +295,6 @@ static const struct snd_soc_dapm_route audio_paths[] = { ...@@ -278,9 +295,6 @@ static const struct snd_soc_dapm_route audio_paths[] = {
{ "Right Output Mixer", "Boost Bypass Switch", "Right Boost Mixer" } , { "Right Output Mixer", "Boost Bypass Switch", "Right Boost Mixer" } ,
{ "Right Output Mixer", "PCM Playback Switch", "Right DAC" }, { "Right Output Mixer", "PCM Playback Switch", "Right DAC" },
{ "Mono Output Mixer", "Left Switch", "Left Output Mixer" },
{ "Mono Output Mixer", "Right Switch", "Right Output Mixer" },
{ "LOUT1 PGA", NULL, "Left Output Mixer" }, { "LOUT1 PGA", NULL, "Left Output Mixer" },
{ "ROUT1 PGA", NULL, "Right Output Mixer" }, { "ROUT1 PGA", NULL, "Right Output Mixer" },
...@@ -297,17 +311,65 @@ static const struct snd_soc_dapm_route audio_paths[] = { ...@@ -297,17 +311,65 @@ static const struct snd_soc_dapm_route audio_paths[] = {
{ "SPK_LP", NULL, "Left Speaker Output" }, { "SPK_LP", NULL, "Left Speaker Output" },
{ "SPK_RN", NULL, "Right Speaker Output" }, { "SPK_RN", NULL, "Right Speaker Output" },
{ "SPK_RP", NULL, "Right Speaker Output" }, { "SPK_RP", NULL, "Right Speaker Output" },
};
static const struct snd_soc_dapm_route audio_paths_out3[] = {
{ "Mono Output Mixer", "Left Switch", "Left Output Mixer" },
{ "Mono Output Mixer", "Right Switch", "Right Output Mixer" },
{ "OUT3", NULL, "Mono Output Mixer", } { "OUT3", NULL, "Mono Output Mixer", }
}; };
static const struct snd_soc_dapm_route audio_paths_capless[] = {
{ "HP_L", NULL, "OUT3 VMID" },
{ "HP_R", NULL, "OUT3 VMID" },
{ "OUT3 VMID", NULL, "Left Output Mixer" },
{ "OUT3 VMID", NULL, "Right Output Mixer" },
};
static int wm8960_add_widgets(struct snd_soc_codec *codec) static int wm8960_add_widgets(struct snd_soc_codec *codec)
{ {
struct wm8960_data *pdata = codec->dev->platform_data;
struct wm8960_priv *wm8960 = codec->private_data;
struct snd_soc_dapm_widget *w;
snd_soc_dapm_new_controls(codec, wm8960_dapm_widgets, snd_soc_dapm_new_controls(codec, wm8960_dapm_widgets,
ARRAY_SIZE(wm8960_dapm_widgets)); ARRAY_SIZE(wm8960_dapm_widgets));
snd_soc_dapm_add_routes(codec, audio_paths, ARRAY_SIZE(audio_paths)); snd_soc_dapm_add_routes(codec, audio_paths, ARRAY_SIZE(audio_paths));
/* In capless mode OUT3 is used to provide VMID for the
* headphone outputs, otherwise it is used as a mono mixer.
*/
if (pdata && pdata->capless) {
snd_soc_dapm_new_controls(codec, wm8960_dapm_widgets_capless,
ARRAY_SIZE(wm8960_dapm_widgets_capless));
snd_soc_dapm_add_routes(codec, audio_paths_capless,
ARRAY_SIZE(audio_paths_capless));
} else {
snd_soc_dapm_new_controls(codec, wm8960_dapm_widgets_out3,
ARRAY_SIZE(wm8960_dapm_widgets_out3));
snd_soc_dapm_add_routes(codec, audio_paths_out3,
ARRAY_SIZE(audio_paths_out3));
}
/* We need to power up the headphone output stage out of
* sequence for capless mode. To save scanning the widget
* list each time to find the desired power state do so now
* and save the result.
*/
list_for_each_entry(w, &codec->dapm_widgets, list) {
if (strcmp(w->name, "LOUT1 PGA") == 0)
wm8960->lout1 = w;
if (strcmp(w->name, "ROUT1 PGA") == 0)
wm8960->rout1 = w;
if (strcmp(w->name, "OUT3 VMID") == 0)
wm8960->out3 = w;
}
return 0; return 0;
} }
...@@ -408,10 +470,9 @@ static int wm8960_mute(struct snd_soc_dai *dai, int mute) ...@@ -408,10 +470,9 @@ static int wm8960_mute(struct snd_soc_dai *dai, int mute)
return 0; return 0;
} }
static int wm8960_set_bias_level(struct snd_soc_codec *codec, static int wm8960_set_bias_level_out3(struct snd_soc_codec *codec,
enum snd_soc_bias_level level) enum snd_soc_bias_level level)
{ {
struct wm8960_data *pdata = codec->dev->platform_data;
u16 reg; u16 reg;
switch (level) { switch (level) {
...@@ -430,18 +491,8 @@ static int wm8960_set_bias_level(struct snd_soc_codec *codec, ...@@ -430,18 +491,8 @@ static int wm8960_set_bias_level(struct snd_soc_codec *codec,
if (codec->bias_level == SND_SOC_BIAS_OFF) { if (codec->bias_level == SND_SOC_BIAS_OFF) {
/* Enable anti-pop features */ /* Enable anti-pop features */
snd_soc_write(codec, WM8960_APOP1, snd_soc_write(codec, WM8960_APOP1,
WM8960_POBCTRL | WM8960_SOFT_ST | WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN | WM8960_BUFIOEN); WM8960_BUFDCOPEN | WM8960_BUFIOEN);
/* Discharge HP output */
reg = WM8960_DISOP;
if (pdata)
reg |= pdata->dres << 4;
snd_soc_write(codec, WM8960_APOP2, reg);
msleep(400);
snd_soc_write(codec, WM8960_APOP2, 0);
/* Enable & ramp VMID at 2x50k */ /* Enable & ramp VMID at 2x50k */
reg = snd_soc_read(codec, WM8960_POWER1); reg = snd_soc_read(codec, WM8960_POWER1);
...@@ -472,8 +523,101 @@ static int wm8960_set_bias_level(struct snd_soc_codec *codec, ...@@ -472,8 +523,101 @@ static int wm8960_set_bias_level(struct snd_soc_codec *codec,
/* Disable VMID and VREF, let them discharge */ /* Disable VMID and VREF, let them discharge */
snd_soc_write(codec, WM8960_POWER1, 0); snd_soc_write(codec, WM8960_POWER1, 0);
msleep(600); msleep(600);
break;
}
codec->bias_level = level;
return 0;
}
static int wm8960_set_bias_level_capless(struct snd_soc_codec *codec,
enum snd_soc_bias_level level)
{
struct wm8960_priv *wm8960 = codec->private_data;
int reg;
switch (level) {
case SND_SOC_BIAS_ON:
break;
case SND_SOC_BIAS_PREPARE:
switch (codec->bias_level) {
case SND_SOC_BIAS_STANDBY:
/* Enable anti pop mode */
snd_soc_update_bits(codec, WM8960_APOP1,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN);
/* Enable LOUT1, ROUT1 and OUT3 if they're enabled */
reg = 0;
if (wm8960->lout1 && wm8960->lout1->power)
reg |= WM8960_PWR2_LOUT1;
if (wm8960->rout1 && wm8960->rout1->power)
reg |= WM8960_PWR2_ROUT1;
if (wm8960->out3 && wm8960->out3->power)
reg |= WM8960_PWR2_OUT3;
snd_soc_update_bits(codec, WM8960_POWER2,
WM8960_PWR2_LOUT1 |
WM8960_PWR2_ROUT1 |
WM8960_PWR2_OUT3, reg);
/* Enable VMID at 2*50k */
snd_soc_update_bits(codec, WM8960_POWER1,
WM8960_VMID_MASK, 0x80);
/* Ramp */
msleep(100);
/* Enable VREF */
snd_soc_update_bits(codec, WM8960_POWER1,
WM8960_VREF, WM8960_VREF);
msleep(100);
break;
case SND_SOC_BIAS_ON:
/* Enable anti-pop mode */
snd_soc_update_bits(codec, WM8960_APOP1,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN);
/* Disable VMID and VREF */
snd_soc_update_bits(codec, WM8960_POWER1,
WM8960_VREF | WM8960_VMID_MASK, 0);
break;
default:
break;
}
break;
case SND_SOC_BIAS_STANDBY:
switch (codec->bias_level) {
case SND_SOC_BIAS_PREPARE:
/* Disable HP discharge */
snd_soc_update_bits(codec, WM8960_APOP2,
WM8960_DISOP | WM8960_DRES_MASK,
0);
/* Disable anti-pop features */
snd_soc_update_bits(codec, WM8960_APOP1,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN,
WM8960_POBCTRL | WM8960_SOFT_ST |
WM8960_BUFDCOPEN);
break;
default:
break;
}
break;
snd_soc_write(codec, WM8960_APOP1, 0); case SND_SOC_BIAS_OFF:
break; break;
} }
...@@ -663,7 +807,7 @@ static int wm8960_suspend(struct platform_device *pdev, pm_message_t state) ...@@ -663,7 +807,7 @@ static int wm8960_suspend(struct platform_device *pdev, pm_message_t state)
struct snd_soc_device *socdev = platform_get_drvdata(pdev); struct snd_soc_device *socdev = platform_get_drvdata(pdev);
struct snd_soc_codec *codec = socdev->card->codec; struct snd_soc_codec *codec = socdev->card->codec;
wm8960_set_bias_level(codec, SND_SOC_BIAS_OFF); codec->set_bias_level(codec, SND_SOC_BIAS_OFF);
return 0; return 0;
} }
...@@ -682,8 +826,8 @@ static int wm8960_resume(struct platform_device *pdev) ...@@ -682,8 +826,8 @@ static int wm8960_resume(struct platform_device *pdev)
codec->hw_write(codec->control_data, data, 2); codec->hw_write(codec->control_data, data, 2);
} }
wm8960_set_bias_level(codec, SND_SOC_BIAS_STANDBY); codec->set_bias_level(codec, SND_SOC_BIAS_STANDBY);
wm8960_set_bias_level(codec, codec->suspend_bias_level); codec->set_bias_level(codec, codec->suspend_bias_level);
return 0; return 0;
} }
...@@ -753,6 +897,8 @@ static int wm8960_register(struct wm8960_priv *wm8960, ...@@ -753,6 +897,8 @@ static int wm8960_register(struct wm8960_priv *wm8960,
goto err; goto err;
} }
codec->set_bias_level = wm8960_set_bias_level_out3;
if (!pdata) { if (!pdata) {
dev_warn(codec->dev, "No platform data supplied\n"); dev_warn(codec->dev, "No platform data supplied\n");
} else { } else {
...@@ -760,6 +906,9 @@ static int wm8960_register(struct wm8960_priv *wm8960, ...@@ -760,6 +906,9 @@ static int wm8960_register(struct wm8960_priv *wm8960,
dev_err(codec->dev, "Invalid DRES: %d\n", pdata->dres); dev_err(codec->dev, "Invalid DRES: %d\n", pdata->dres);
pdata->dres = 0; pdata->dres = 0;
} }
if (pdata->capless)
codec->set_bias_level = wm8960_set_bias_level_capless;
} }
mutex_init(&codec->mutex); mutex_init(&codec->mutex);
...@@ -770,7 +919,6 @@ static int wm8960_register(struct wm8960_priv *wm8960, ...@@ -770,7 +919,6 @@ static int wm8960_register(struct wm8960_priv *wm8960,
codec->name = "WM8960"; codec->name = "WM8960";
codec->owner = THIS_MODULE; codec->owner = THIS_MODULE;
codec->bias_level = SND_SOC_BIAS_OFF; codec->bias_level = SND_SOC_BIAS_OFF;
codec->set_bias_level = wm8960_set_bias_level;
codec->dai = &wm8960_dai; codec->dai = &wm8960_dai;
codec->num_dai = 1; codec->num_dai = 1;
codec->reg_cache_size = WM8960_CACHEREGNUM; codec->reg_cache_size = WM8960_CACHEREGNUM;
...@@ -792,7 +940,7 @@ static int wm8960_register(struct wm8960_priv *wm8960, ...@@ -792,7 +940,7 @@ static int wm8960_register(struct wm8960_priv *wm8960,
wm8960_dai.dev = codec->dev; wm8960_dai.dev = codec->dev;
wm8960_set_bias_level(codec, SND_SOC_BIAS_STANDBY); codec->set_bias_level(codec, SND_SOC_BIAS_STANDBY);
/* Latch the update bits */ /* Latch the update bits */
reg = snd_soc_read(codec, WM8960_LINVOL); reg = snd_soc_read(codec, WM8960_LINVOL);
...@@ -841,7 +989,7 @@ static int wm8960_register(struct wm8960_priv *wm8960, ...@@ -841,7 +989,7 @@ static int wm8960_register(struct wm8960_priv *wm8960,
static void wm8960_unregister(struct wm8960_priv *wm8960) static void wm8960_unregister(struct wm8960_priv *wm8960)
{ {
wm8960_set_bias_level(&wm8960->codec, SND_SOC_BIAS_OFF); wm8960->codec.set_bias_level(&wm8960->codec, SND_SOC_BIAS_OFF);
snd_soc_unregister_dai(&wm8960_dai); snd_soc_unregister_dai(&wm8960_dai);
snd_soc_unregister_codec(&wm8960->codec); snd_soc_unregister_codec(&wm8960->codec);
kfree(wm8960); kfree(wm8960);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment