[alsa-devel][PATCH v2] ASoC: wm8960: update pll and clock setting function

From: Zidan Wang
Date: Fri Jul 03 2015 - 05:12:26 EST


Add sysclk auto mode. When it's sysclk auto mode, if the MCLK is
available for clock configure, using MCLK to provide sysclk directly,
otherwise, search a available pll out frequcncy and set pll.

TX and RX share the same sysclk and bclk divider register, and have
different DAC/ADC divier register. When playback and capture
simultaneously, the second stream should not set pll, should not
change the sysclk and bclk, just need set DAC/ADC divider to support
different TX and RX sample rate.

Signed-off-by: Zidan Wang <zidan.wang@xxxxxxxxxxxxx>
---
sound/soc/codecs/wm8960.c | 256 ++++++++++++++++++++++++++++++++++++++--------
sound/soc/codecs/wm8960.h | 1 +
2 files changed, 214 insertions(+), 43 deletions(-)

diff --git a/sound/soc/codecs/wm8960.c b/sound/soc/codecs/wm8960.c
index 94c5c46..e170bdf 100644
--- a/sound/soc/codecs/wm8960.c
+++ b/sound/soc/codecs/wm8960.c
@@ -48,6 +48,9 @@
#define WM8960_DISOP 0x40
#define WM8960_DRES_MASK 0x30

+static bool is_pll_freq_available(unsigned int source, unsigned int target);
+static int wm8960_set_pll(struct snd_soc_dai *codec_dai,
+ unsigned int freq_in, unsigned int freq_out);
/*
* wm8960 register cache
* We can't read the WM8960 register space when we are
@@ -126,9 +129,12 @@ struct wm8960_priv {
struct snd_soc_dapm_widget *rout1;
struct snd_soc_dapm_widget *out3;
bool deemph;
- int playback_fs;
- int bclk;
int sysclk;
+ int clk_id;
+ int freq_in;
+ int fs[2];
+ int dac_div[2];
+ bool is_stream_in_use[2];
struct wm8960_data pdata;
};

@@ -164,8 +170,8 @@ static int wm8960_set_deemph(struct snd_soc_codec *codec)
if (wm8960->deemph) {
best = 1;
for (i = 2; i < ARRAY_SIZE(deemph_settings); i++) {
- if (abs(deemph_settings[i] - wm8960->playback_fs) <
- abs(deemph_settings[best] - wm8960->playback_fs))
+ if (abs(deemph_settings[i] - wm8960->fs[1]) <
+ abs(deemph_settings[best] - wm8960->fs[1]))
best = i;
}

@@ -565,6 +571,9 @@ static struct {
{ 8000, 5 },
};

+/* -1 for reserved value */
+static const int sysclk_divs[] = { 1, -1, 2, -1 };
+
/* Multiply 256 for internal 256 div */
static const int dac_divs[] = { 256, 384, 512, 768, 1024, 1408, 1536 };

@@ -574,61 +583,166 @@ static const int bclk_divs[] = {
120, 160, 220, 240, 320, 320, 320
};

-static void wm8960_configure_clocking(struct snd_soc_codec *codec,
- bool tx, int lrclk)
+static int wm8960_configure_clocking(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params, struct snd_soc_dai *dai)
{
+ struct snd_soc_codec *codec = dai->codec;
struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
+ unsigned int sample_rate = params_rate(params);
+ unsigned int channels = params_channels(params);
+ unsigned int sysclk, bclk, freq_out, freq_in;
+ bool tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
u16 iface1 = snd_soc_read(codec, WM8960_IFACE1);
u16 iface2 = snd_soc_read(codec, WM8960_IFACE2);
- u32 sysclk;
- int i, j;
+ int i, j, k;

if (!(iface1 & (1<<6))) {
dev_dbg(codec->dev,
"Codec is slave mode, no need to configure clock\n");
- return;
+ return 0;
+ }
+
+ /*
+ * playback and capture using the same registers of bclk and sysclk
+ * div, and different registers for dac/adc div. If playback and capture
+ * simultaneously, the second should not set pll, should not change
+ * the sysclk and bclk, only dac/adc div can be set to support different
+ * frame clock.
+ */
+ if (wm8960->is_stream_in_use[!tx]) {
+ /*
+ * If ADCLRC configure as GPIO pin, DACLRC pin is used as a
+ * frame clock for ADCs and DACs.
+ */
+ if (iface2 & (1 << 6)) {
+ snd_soc_update_bits(codec, WM8960_CLOCK1,
+ 0x7 << 3, wm8960->dac_div[!tx] << 3);
+ return 0;
+ }
+
+ /*
+ * If the different TX and RX sample rate can be support,
+ * setting corresponding DAC or ADC divider.
+ */
+ for (i = 0; i < ARRAY_SIZE(dac_divs); ++i) {
+ if (wm8960->fs[!tx] * dac_divs[wm8960->dac_div[!tx]]
+ == wm8960->fs[tx] * dac_divs[i]) {
+ if (tx)
+ snd_soc_update_bits(codec,
+ WM8960_CLOCK1,
+ 0x7 << 3, dac_divs[i] << 3);
+ else
+ snd_soc_update_bits(codec,
+ WM8960_CLOCK1,
+ 0x7 << 6, dac_divs[i] << 6);
+ wm8960->dac_div[tx] = dac_divs[i];
+ return 0;
+ }
+ }
+ if (i == ARRAY_SIZE(dac_divs)) {
+ dev_err(codec->dev,
+ "can't support TX=%d RX=%d\n simultaneously",
+ wm8960->fs[1], wm8960->fs[0]);
+ return -EINVAL;
+ }
+ }
+
+ if (wm8960->clk_id != WM8960_SYSCLK_MCLK && !wm8960->freq_in) {
+ dev_err(codec->dev, "No MCLK configured\n");
+ return -EINVAL;
}

- if (!wm8960->sysclk) {
- dev_dbg(codec->dev, "No SYSCLK configured\n");
- return;
+ freq_in = wm8960->freq_in;
+
+ bclk = snd_soc_params_to_bclk(params);
+ if (channels == 1)
+ bclk *= 2;
+
+ /*
+ * If it's sysclk auto mode, check if the MCLK can provide sysclk or
+ * not. If MCLK can provide sysclk, using MCLK to provide sysclk
+ * directly. Otherwise, auto select a available pll out frequency
+ * and set PLL.
+ */
+ if (wm8960->clk_id == WM8960_SYSCLK_AUTO) {
+ /* disable the PLL and using MCLK to provide sysclk */
+ wm8960_set_pll(dai, 0, 0);
+ freq_out = freq_in;
+ } else if (wm8960->sysclk)
+ freq_out = wm8960->sysclk;
+ else {
+ dev_err(codec->dev, "No SYSCLK configured\n");
+ return -EINVAL;
}

- if (!wm8960->bclk || !lrclk) {
- dev_dbg(codec->dev, "No audio clocks configured\n");
- return;
+ /* check if the sysclk frequency is available. */
+ for (i = 0; i < ARRAY_SIZE(sysclk_divs); ++i) {
+ if (sysclk_divs[i] == -1)
+ continue;
+ sysclk = freq_out / sysclk_divs[i];
+ for (j = 0; j < ARRAY_SIZE(dac_divs); ++j) {
+ if (sysclk == dac_divs[j] * sample_rate) {
+ for (k = 0; k < ARRAY_SIZE(bclk_divs); ++k)
+ if (sysclk == bclk * bclk_divs[k] / 10)
+ break;
+ if (k != ARRAY_SIZE(bclk_divs))
+ break;
+ }
+ }
+ if (j != ARRAY_SIZE(dac_divs))
+ break;
}

- for (i = 0; i < ARRAY_SIZE(dac_divs); ++i) {
- if (wm8960->sysclk == lrclk * dac_divs[i]) {
- for (j = 0; j < ARRAY_SIZE(bclk_divs); ++j) {
- sysclk = wm8960->bclk * bclk_divs[j] / 10;
- if (wm8960->sysclk == sysclk)
+ if (i != ARRAY_SIZE(sysclk_divs))
+ goto configure_clock;
+ else if (wm8960->clk_id != WM8960_SYSCLK_AUTO) {
+ dev_err(codec->dev, "failed to configure clock\n");
+ return -EINVAL;
+ }
+ /* get a available pll out frequency and set pll */
+ for (i = 0; i < ARRAY_SIZE(sysclk_divs); ++i) {
+ if (sysclk_divs[i] == -1)
+ continue;
+ for (j = 0; j < ARRAY_SIZE(dac_divs); ++j) {
+ sysclk = sample_rate * dac_divs[j];
+ freq_out = sysclk * sysclk_divs[i];
+
+ for (k = 0; k < ARRAY_SIZE(bclk_divs); ++k) {
+ if (sysclk == bclk * bclk_divs[k] / 10 &&
+ is_pll_freq_available(freq_in, freq_out)) {
+ wm8960_set_pll(dai, freq_in, freq_out);
break;
+ } else
+ continue;
}
- if(j != ARRAY_SIZE(bclk_divs))
+ if (k != ARRAY_SIZE(bclk_divs))
break;
}
+ if (j != ARRAY_SIZE(dac_divs))
+ break;
}

- if (i == ARRAY_SIZE(dac_divs)) {
- dev_err(codec->dev, "Unsupported sysclk %d\n", wm8960->sysclk);
- return;
+ if (i == ARRAY_SIZE(sysclk_divs)) {
+ dev_err(codec->dev, "failed to configure clock\n");
+ return -EINVAL;
}

- /*
- * configure frame clock. If ADCLRC configure as GPIO pin, DACLRC
- * pin is used as a frame clock for ADCs and DACs.
- */
- if (iface2 & (1<<6))
- snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 3, i << 3);
+configure_clock:
+ snd_soc_update_bits(codec, WM8960_CLOCK1, 3 << 1, i << 1);
+
+ if (iface2 & (1 << 6))
+ snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 3, j << 3);
else if (tx)
- snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 3, i << 3);
+ snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 3, j << 3);
else if (!tx)
- snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 6, i << 6);
+ snd_soc_update_bits(codec, WM8960_CLOCK1, 0x7 << 6, j << 6);

/* configure bit clock */
- snd_soc_update_bits(codec, WM8960_CLOCK2, 0xf, j);
+ snd_soc_update_bits(codec, WM8960_CLOCK2, 0xf, k);
+
+ wm8960->dac_div[tx] = j;
+
+ return 0;
}

static int wm8960_hw_params(struct snd_pcm_substream *substream,
@@ -641,10 +755,6 @@ static int wm8960_hw_params(struct snd_pcm_substream *substream,
bool tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
int i;

- wm8960->bclk = snd_soc_params_to_bclk(params);
- if (params_channels(params) == 1)
- wm8960->bclk *= 2;
-
/* bit size */
switch (params_width(params)) {
case 16:
@@ -667,11 +777,11 @@ static int wm8960_hw_params(struct snd_pcm_substream *substream,
return -EINVAL;
}

+ wm8960->fs[tx] = params_rate(params);
/* Update filters for the new rate */
- if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
- wm8960->playback_fs = params_rate(params);
+ if (tx)
wm8960_set_deemph(codec);
- } else {
+ else {
for (i = 0; i < ARRAY_SIZE(alc_rates); i++)
if (alc_rates[i].rate == params_rate(params))
snd_soc_update_bits(codec,
@@ -682,7 +792,27 @@ static int wm8960_hw_params(struct snd_pcm_substream *substream,
/* set iface */
snd_soc_write(codec, WM8960_IFACE1, iface);

- wm8960_configure_clocking(codec, tx, params_rate(params));
+ wm8960->is_stream_in_use[tx] = true;
+
+ return wm8960_configure_clocking(substream, params, dai);
+}
+
+static int wm8960_hw_free(struct snd_pcm_substream *substream,
+ struct snd_soc_dai *dai)
+{
+ struct snd_soc_codec *codec = dai->codec;
+ struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
+ u16 clock1 = snd_soc_read(codec, WM8960_CLOCK1);
+ bool tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
+
+ wm8960->is_stream_in_use[tx] = false;
+ /*
+ * If it's sysclk auto mode, no stream in use, and the pll is enabled,
+ * disable the pll
+ */
+ if (wm8960->clk_id == WM8960_SYSCLK_AUTO && (clock1 & 0x1) &&
+ !wm8960->is_stream_in_use[!tx])
+ wm8960_set_pll(dai, 0, 0);

return 0;
}
@@ -892,6 +1022,28 @@ struct _pll_div {
u32 k:24;
};

+static bool is_pll_freq_available(unsigned int source, unsigned int target)
+{
+ unsigned int Ndiv;
+
+ if (source == 0 || target == 0)
+ return false;
+
+ /* Scale up target to PLL operating frequency */
+ target *= 4;
+ Ndiv = target / source;
+
+ if (Ndiv < 6) {
+ source >>= 1;
+ Ndiv = target / source;
+ }
+
+ if ((Ndiv < 6) || (Ndiv > 12))
+ return false;
+
+ return true;
+}
+
/* The size in bits of the pll divide multiplied by 10
* to allow rounding later */
#define FIXED_PLL_SIZE ((1 << 24) * 10)
@@ -943,8 +1095,8 @@ static int pll_factors(unsigned int source, unsigned int target,
return 0;
}

-static int wm8960_set_dai_pll(struct snd_soc_dai *codec_dai, int pll_id,
- int source, unsigned int freq_in, unsigned int freq_out)
+static int wm8960_set_pll(struct snd_soc_dai *codec_dai,
+ unsigned int freq_in, unsigned int freq_out)
{
struct snd_soc_codec *codec = codec_dai->codec;
u16 reg;
@@ -986,6 +1138,20 @@ static int wm8960_set_dai_pll(struct snd_soc_dai *codec_dai, int pll_id,
return 0;
}

+static int wm8960_set_dai_pll(struct snd_soc_dai *codec_dai, int pll_id,
+ int source, unsigned int freq_in, unsigned int freq_out)
+{
+ struct snd_soc_codec *codec = codec_dai->codec;
+ struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
+
+ wm8960->freq_in = freq_in;
+
+ if (wm8960->clk_id == WM8960_SYSCLK_AUTO)
+ return 0;
+
+ return wm8960_set_pll(codec_dai, freq_in, freq_out);
+}
+
static int wm8960_set_dai_clkdiv(struct snd_soc_dai *codec_dai,
int div_id, int div)
{
@@ -1043,11 +1209,14 @@ static int wm8960_set_dai_sysclk(struct snd_soc_dai *dai, int clk_id,
snd_soc_update_bits(codec, WM8960_CLOCK1,
0x1, WM8960_SYSCLK_PLL);
break;
+ case WM8960_SYSCLK_AUTO:
+ break;
default:
return -EINVAL;
}

wm8960->sysclk = freq;
+ wm8960->clk_id = clk_id;

return 0;
}
@@ -1060,6 +1229,7 @@ static int wm8960_set_dai_sysclk(struct snd_soc_dai *dai, int clk_id,

static const struct snd_soc_dai_ops wm8960_dai_ops = {
.hw_params = wm8960_hw_params,
+ .hw_free = wm8960_hw_free,
.digital_mute = wm8960_mute,
.set_fmt = wm8960_set_dai_fmt,
.set_clkdiv = wm8960_set_dai_clkdiv,
diff --git a/sound/soc/codecs/wm8960.h b/sound/soc/codecs/wm8960.h
index 2d8163d..ab3220d 100644
--- a/sound/soc/codecs/wm8960.h
+++ b/sound/soc/codecs/wm8960.h
@@ -82,6 +82,7 @@

#define WM8960_SYSCLK_MCLK (0 << 0)
#define WM8960_SYSCLK_PLL (1 << 0)
+#define WM8960_SYSCLK_AUTO (2 << 0)

#define WM8960_DAC_DIV_1 (0 << 3)
#define WM8960_DAC_DIV_1_5 (1 << 3)
--
1.9.1

--
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/