A new implementation for FLLs as contained in WM8904, WM8994 and a few other Cirrus Logic (formerly Wolfson) codecs. Patches using this common code follow.
Signed-off-by: Michał Mirosław mirq-linux@rere.qmqm.pl --- sound/soc/codecs/Kconfig | 6 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/wm_fll.c | 518 ++++++++++++++++++++++++++++++++++++++ sound/soc/codecs/wm_fll.h | 60 +++++ 4 files changed, 586 insertions(+)
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 9f89a5346299..04086acf6d93 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -281,6 +281,12 @@ config SND_SOC_ARIZONA default m if SND_SOC_WM8997=m default m if SND_SOC_WM8998=m
+config SND_SOC_WM_FLL + tristate + +config SND_SOC_WM_FLL_EFS + bool + config SND_SOC_WM_HUBS tristate default y if SND_SOC_WM8993=y || SND_SOC_WM8994=y diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 5b4bb8cf4325..22704fbb7497 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -269,6 +269,7 @@ snd-soc-wm9090-objs := wm9090.o snd-soc-wm9705-objs := wm9705.o snd-soc-wm9712-objs := wm9712.o snd-soc-wm9713-objs := wm9713.o +snd-soc-wm-fll-objs := wm_fll.o snd-soc-wm-hubs-objs := wm_hubs.o snd-soc-zx-aud96p22-objs := zx_aud96p22.o # Amp @@ -549,6 +550,7 @@ obj-$(CONFIG_SND_SOC_WM9705) += snd-soc-wm9705.o obj-$(CONFIG_SND_SOC_WM9712) += snd-soc-wm9712.o obj-$(CONFIG_SND_SOC_WM9713) += snd-soc-wm9713.o obj-$(CONFIG_SND_SOC_WM_ADSP) += snd-soc-wm-adsp.o +obj-$(CONFIG_SND_SOC_WM_FLL) += snd-soc-wm-fll.o obj-$(CONFIG_SND_SOC_WM_HUBS) += snd-soc-wm-hubs.o obj-$(CONFIG_SND_SOC_ZX_AUD96P22) += snd-soc-zx-aud96p22.o
diff --git a/sound/soc/codecs/wm_fll.c b/sound/soc/codecs/wm_fll.c new file mode 100644 index 000000000000..0d8217287030 --- /dev/null +++ b/sound/soc/codecs/wm_fll.c @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * wm_fll.c -- WM89xx FLL support + * + * Copyright 2019 Michał Mirosław + * + * WM can generate its clock directly from MCLK, from + * internal FLL synchronizing to one of hw frame clocks + * or from FLL's VCO in free-running mode + */ + +#include <linux/bitfield.h> +#include <linux/clk-provider.h> +#include <linux/delay.h> +#include <linux/gcd.h> +#include <linux/kernel.h> + +#include "wm_fll.h" + +/* FLL Control 1 */ +#define WM_FLL_CONTROL_1 (hw->desc->ctl_offset + 0) +#define WM_FLL_FRACN_ENA BIT(2) +#define WM_FLL_OSC_ENA BIT(1) +#define WM_FLL_ENA BIT(0) + +/* FLL Control 2 */ +#define WM_FLL_CONTROL_2 (hw->desc->ctl_offset + 1) +#define WM_FLL_OUTDIV GENMASK(13, 8) +#define WM_FLL_CTRL_RATE GENMASK(6, 4) +#define WM_FLL_FRATIO GENMASK(2, 0) + +/* FLL Control 3 */ +#define WM_FLL_CONTROL_3 (hw->desc->ctl_offset + 2) +#define WM_FLL_K GENMASK(15, 0) + +/* FLL Control 4 */ +#define WM_FLL_CONTROL_4 (hw->desc->ctl_offset + 3) +#define WM_FLL_N GENMASK(14, 5) +#define WM_FLL_GAIN GENMASK(3, 0) + +/* FLL Control 5 */ +#define WM_FLL_CONTROL_5 (hw->desc->ctl_offset + 4) +#define WM_FLL_CLK_REF_DIV GENMASK(4, 3) +#define WM_FLL_CLK_REF_SRC GENMASK(1, 0) + +/* Interrupt Status */ +#define WM_INTERRUPT_STATUS (hw->desc->int_offset + 0) + +/* FLL NCO Test 0 (part of FLL Control 5 on some chips) */ +#define WM_FLL_NCO_TEST_0 (hw->desc->nco_reg0) +#define WM_FLL_FRC_NCO BIT(0) + +/* FLL NCO Test 1 (part of FLL Control 5 on some chips) */ +#define WM_FLL_NCO_TEST_1 (hw->desc->nco_reg1) +#define WM_FLL_FRC_NCO_VAL GENMASK(5, 0) + +/* FLL EFS 1 (chips with N+THETA/LAMBDA instead of N.K multiplier) */ +#define WM_FLL_EFS_1 (hw->desc->efs_offset + 0) +#define WM_FLL_LAMBDA GENMASK(15, 0) + +/* FLL EFS 2 (chips with N+THETA/LAMBDA instead of N.K multiplier) */ +#define WM_FLL_EFS_2 (hw->desc->efs_offset + 1) +#define WM_FLL_EFS_ENA BIT(0) + + +/* feature tests */ +#define WM_FLL_USES_EFS(hw) (IS_ENABLED(CONFIG_SND_SOC_WM_FLL_EFS) && hw->desc->efs_offset) + + +static bool wm_fll_in_free_running_mode(struct wm_fll_data *hw) +{ + unsigned int val; + + if (!hw->desc->nco_reg0) + return false; + if (regmap_read(hw->regmap, WM_FLL_NCO_TEST_0, &val) < 0) + return false; + + val >>= hw->desc->frc_nco_shift; + return FIELD_GET(WM_FLL_FRC_NCO, val); +} + +static int wm_fll_set_free_running_mode(struct wm_fll_data *hw, bool enable) +{ + unsigned int val, mask; + int err; + + if (!hw->desc->nco_reg0) + return enable ? -EINVAL : 0; + + if (enable) { + /* set osc freq (approx 96MHz) */ + val = FIELD_PREP(WM_FLL_FRC_NCO_VAL, 0x19); + mask = WM_FLL_FRC_NCO_VAL; + + val <<= hw->desc->frc_nco_val_shift; + mask <<= hw->desc->frc_nco_val_shift; + + err = regmap_update_bits(hw->regmap, WM_FLL_NCO_TEST_1, + mask, val); + if (err) + return err; + } + + /* set free-running mode */ + val = FIELD_PREP(WM_FLL_FRC_NCO, enable); + mask = WM_FLL_FRC_NCO; + + val <<= hw->desc->frc_nco_shift; + mask <<= hw->desc->frc_nco_shift; + + return regmap_update_bits(hw->regmap, WM_FLL_NCO_TEST_0, + mask, val); +} + +static int wm_fll_get_parent(struct wm_fll_data *hw) +{ + unsigned int val; + int err; + + /* free-running mode? */ + if (wm_fll_in_free_running_mode(hw)) + return FLL_REF_OSC; + + err = regmap_read(hw->regmap, WM_FLL_CONTROL_5, &val); + if (err < 0) + return err; + + val = FIELD_GET(WM_FLL_CLK_REF_SRC, val); + return hw->desc->clk_ref_map[val]; +} + +/** + * wm_fll_set_parent() - Change FLL clock source + * + * @hw: FLL hardware info + * @index: FLL source clock id + * + * Configures FLL for using @index clock as input. + * + * Return 0 if successful, error code if not. + */ +int wm_fll_set_parent(struct wm_fll_data *hw, enum wm_fll_ref_source index) +{ + unsigned int ref; + bool osc_en; + int err; + + osc_en = index == FLL_REF_OSC; + err = wm_fll_set_free_running_mode(hw, osc_en); + if (osc_en || err) + return err; + + err = -EINVAL; + for (ref = 0; ref < ARRAY_SIZE(hw->desc->clk_ref_map); ++ref) { + if (hw->desc->clk_ref_map[ref] != index) + continue; + + err = 0; + break; + } + if (err < 0) + return err; + + /* set FLL reference input */ + ref = FIELD_PREP(WM_FLL_CLK_REF_SRC, ref); + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_5, + WM_FLL_CLK_REF_SRC, ref); + return err; +} +EXPORT_SYMBOL_GPL(wm_fll_set_parent); + +/** + * wm_fll_enable() - Enable FLL + * + * @hw: initialized FLL hardware info + * + * Requests source clock and starts the FLL. + * Waits for lock before returning. + * + * Return 0 if successful, error code if not. + */ +int wm_fll_enable(struct wm_fll_data *hw) +{ + unsigned int val; + int clk_src; + int err, retry; + + err = clk_src = wm_fll_get_parent(hw); + if (err < 0) + return err; + + if (clk_src == FLL_REF_MCLK) { + err = clk_prepare_enable(hw->mclk); + if (err < 0) + return err; + } + + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_1, + WM_FLL_OSC_ENA, WM_FLL_OSC_ENA); + if (err < 0) + goto err_out; + + err = regmap_write(hw->regmap, WM_INTERRUPT_STATUS, + hw->desc->int_mask); + if (err < 0) + goto err_out; + + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_1, + WM_FLL_ENA, WM_FLL_ENA); + if (err) + goto err_out; + + if (clk_src == FLL_REF_OSC) { + usleep_range(150, 250); + return 0; + } + + for (retry = 3; retry; --retry) { + msleep(1); + err = regmap_read(hw->regmap, WM_INTERRUPT_STATUS, &val); + if (err < 0) + goto err_out; + + if (val & hw->desc->int_mask) + break; + } + + /* it seems that FLL_LOCK might never be asserted */ + /* eg. WM8904's FLL doesn't, but works anyway */ + return 0; + +err_out: + wm_fll_disable(hw); + + if (clk_src == FLL_REF_MCLK) + clk_disable_unprepare(hw->mclk); + + return err; +} +EXPORT_SYMBOL_GPL(wm_fll_enable); + +/** + * wm_fll_disable() - Disable FLL + * + * @hw: initialized FLL hardware info + * + * Return 0 if successful, error code if not. + */ +int wm_fll_disable(struct wm_fll_data *hw) +{ + return regmap_update_bits(hw->regmap, WM_FLL_CONTROL_1, + WM_FLL_ENA|WM_FLL_OSC_ENA, + 0); +} +EXPORT_SYMBOL_GPL(wm_fll_disable); + +/** + * wm_fll_is_enabled() - Check whether FLL is enabled + * + * @hw: initialized FLL hardware info + * + * Returns 0 if disabled, 1 if enabled, or negative error code. + */ +int wm_fll_is_enabled(struct wm_fll_data *hw) +{ + unsigned int val; + int err; + + err = regmap_read(hw->regmap, WM_FLL_CONTROL_1, &val); + if (err < 0) + return err; + + return FIELD_GET(WM_FLL_ENA, val); +} +EXPORT_SYMBOL_GPL(wm_fll_is_enabled); + +static unsigned int wm_fll_apply_refdiv(unsigned long *parent_rate) +{ + unsigned int refdiv; + + /* FLL input divider; should ensure Fin <= 13.5MHz */ + + refdiv = DIV_ROUND_UP(*parent_rate, 13500000); + refdiv = order_base_2(refdiv); + if (refdiv > 3) + refdiv = 3; + + *parent_rate >>= refdiv; + + return refdiv; +} + +static unsigned int wm_fll_apply_fratio(unsigned long *parent_rate) +{ + unsigned int fratio; + + /* FLL comparator divider; efectively Fin multiplier */ + /* as tabularized in WM datasheet */ + + if (*parent_rate >= 256000) + fratio = *parent_rate < 1024000; + else if (*parent_rate >= 64000) + fratio = 2 + (*parent_rate < 128000); + else + fratio = 4; + + *parent_rate <<= fratio; + + return fratio; +} + +static unsigned int wm_fll_apply_outdiv_rev(unsigned long *rate) +{ + unsigned int div; + + /* Fvco -> Fout divider; target: 90 <= Fvco <= 100 MHz */ + + div = DIV_ROUND_UP(90000000, *rate); + if (div > 64) { + *rate = 90000000; + return 64; + } + + if (div < 4) + div = 4; + + *rate *= div; + return div; +} + +static unsigned long wm_fll_apply_frac(struct wm_fll_data *hw, + unsigned long rate_in, unsigned long *rate_out, + unsigned int *kdiv_out) +{ + unsigned long long freq; + unsigned long rate = *rate_out; + unsigned int kdiv = 0x10000; + + if (WM_FLL_USES_EFS(hw)) { + unsigned int cd, num; + + cd = gcd(rate, rate_in); + freq = rate / rate_in; + num = rate - freq * rate_in; + num /= cd; + kdiv = rate_in / cd; + + rate = freq * rate_in + num * rate_in / kdiv; + freq = (freq << 16) | num; + } else { + freq = (unsigned long long)rate << 16; + freq += rate_in / 2; + do_div(freq, rate_in); + + rate = (freq * rate_in) >> 16; + } + + *rate_out = rate; + *kdiv_out = kdiv; + return freq; +} + +/** + * wm_fll_set_rate() - Configures FLL for specified bitrate + * + * @hw: initialized FLL hardware info + * @rate: bitrate to configure for + * + * Return 0 if successful, error code if not. FLL must be disabled + * on entry. + */ +int wm_fll_set_rate(struct wm_fll_data *hw, unsigned long rate) +{ + unsigned long freq, mclk_rate; + unsigned int val, mask, refdiv, outdiv, fratio, kdiv; + int err; + + err = wm_fll_is_enabled(hw); + if (err < 0) + return err; + if (err > 0) + return -EBUSY; + + err = wm_fll_get_parent(hw); + if (err < 0) + return err; + + if (err != FLL_REF_OSC) { + unsigned long parent_rate; + + if (hw->mclk) + hw->freq_in = clk_get_rate(hw->mclk); + + parent_rate = mclk_rate = hw->freq_in; + refdiv = wm_fll_apply_refdiv(&parent_rate); + fratio = wm_fll_apply_fratio(&parent_rate); + outdiv = wm_fll_apply_outdiv_rev(&rate); + freq = wm_fll_apply_frac(hw, parent_rate, &rate, &kdiv); + } else { + unsigned long vco_rate = 96000000; + + mclk_rate = 0; + fratio = refdiv = 0; + rate = DIV_ROUND_CLOSEST(vco_rate, rate); + outdiv = clamp_t(unsigned long, rate, 4, 64); + freq = 0x177 << 16; + kdiv = 0; + + rate = vco_rate; + } + + /* configure */ + + dev_dbg(regmap_get_device(hw->regmap), + "configuring FLL for %luHz -> %luHz -> %luHz\n", + mclk_rate, rate, rate / outdiv); + dev_dbg(regmap_get_device(hw->regmap), + "FLL settings: N=%lu K=%lu/%u FRATIO=%u OUTDIV=%u REF_DIV=%u\n", + freq >> 16, freq & 0xFFFF, kdiv, fratio, outdiv, refdiv); + + val = FIELD_PREP(WM_FLL_CLK_REF_DIV, refdiv); + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_5, + WM_FLL_CLK_REF_DIV, val); + if (err < 0) + return err; + + val = FIELD_PREP(WM_FLL_OUTDIV, outdiv - 1) | + FIELD_PREP(WM_FLL_FRATIO, fratio); + mask = WM_FLL_OUTDIV | WM_FLL_FRATIO; + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_2, mask, val); + if (err < 0) + return err; + + err = regmap_write(hw->regmap, WM_FLL_CONTROL_3, (uint16_t)freq); + if (err < 0) + return err; + + if (WM_FLL_USES_EFS(hw)) { + val = FIELD_PREP(WM_FLL_EFS_ENA, !!(uint16_t)freq); + err = regmap_update_bits(hw->regmap, WM_FLL_EFS_2, + WM_FLL_EFS_ENA, val); + if (err < 0) + return err; + + val = FIELD_PREP(WM_FLL_LAMBDA, kdiv); + err = regmap_update_bits(hw->regmap, WM_FLL_EFS_1, + WM_FLL_LAMBDA, val); + } else { + val = FIELD_PREP(WM_FLL_FRACN_ENA, !!(uint16_t)freq); + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_1, + WM_FLL_FRACN_ENA, val); + } + if (err < 0) + return err; + + val = FIELD_PREP(WM_FLL_N, freq >> 16); + err = regmap_update_bits(hw->regmap, WM_FLL_CONTROL_4, + WM_FLL_N, val); + return err; +} +EXPORT_SYMBOL_GPL(wm_fll_set_rate); + +/** + * wm_fll_init() - Initialize FLL + * + * @hw: FLL hardware info + * + * Checks and initializes FLL structure. + * Requires hw->desc and hw->regmap to be filled in by caller. + * + * Return 0 if successful, negative error code if not. + * A message is logged on error. + */ +int wm_fll_init(struct wm_fll_data *hw) +{ + if (!IS_ENABLED(CONFIG_SND_SOC_WM_FLL_EFS) && hw->desc->efs_offset) { + struct device *dev = regmap_get_device(hw->regmap); + + dev_err(dev, "FLL EFS support not compiled in\n"); + return -ENOSYS; + } + + return 0; +} +EXPORT_SYMBOL_GPL(wm_fll_init); + +/** + * wm_fll_init_with_clk() - Initialize FLL + * + * @hw: FLL hardware info + * + * Checks and initializes FLL described in @hw, and requests MCLK input clock. + * Requires hw->desc and hw->regmap to be filled in by caller. + * + * Return 0 if successful, negative error code if not. + * A message is logged on error. + */ +int wm_fll_init_with_clk(struct wm_fll_data *hw) +{ + struct device *dev = regmap_get_device(hw->regmap); + int err; + + err = wm_fll_init(hw); + if (err) + return err; + + hw->mclk = devm_clk_get(dev, "mclk"); + if (IS_ERR(hw->mclk)) { + err = PTR_ERR(hw->mclk); + dev_err(dev, "Failed to get MCLK for FLL @0x%x: %d\n", + hw->desc->ctl_offset, err); + return err; + } + + return 0; +} +EXPORT_SYMBOL_GPL(wm_fll_init_with_clk); diff --git a/sound/soc/codecs/wm_fll.h b/sound/soc/codecs/wm_fll.h new file mode 100644 index 000000000000..8519a8691397 --- /dev/null +++ b/sound/soc/codecs/wm_fll.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * wm_fll.h -- FLL support for Wolfson codecs + * + * Copyright 2019 Michał Mirosław + */ + +#ifndef _WM_FLL_H +#define _WM_FLL_H + +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/regmap.h> + +enum wm_fll_ref_source +{ + FLL_REF_MCLK = 1, + FLL_REF_MCLK2, + FLL_REF_BCLK, + FLL_REF_FSCLK, + FLL_REF_OSC, +}; + +/** + * struct wm_fll_desc - FLL variant description + * + * @offset: FLL control register block offset + * @clk_ref_map: FLL_REF_* assignment for each of FLL.REF_SRC field value + */ +struct wm_fll_desc +{ + uint16_t ctl_offset; + uint16_t int_offset; + uint16_t int_mask; + uint16_t nco_reg0; + uint16_t nco_reg1; + uint8_t frc_nco_shift; + uint8_t frc_nco_val_shift; + uint16_t efs_offset; + uint8_t clk_ref_map[4]; +}; + +struct wm_fll_data +{ + const struct wm_fll_desc *desc; + struct regmap *regmap; + unsigned long freq_in; + + struct clk *mclk; +}; + +int wm_fll_init(struct wm_fll_data *hw); +int wm_fll_init_with_clk(struct wm_fll_data *hw); +int wm_fll_is_enabled(struct wm_fll_data *hw); +int wm_fll_enable(struct wm_fll_data *hw); +int wm_fll_disable(struct wm_fll_data *hw); +int wm_fll_set_parent(struct wm_fll_data *hw, enum wm_fll_ref_source index); +int wm_fll_set_rate(struct wm_fll_data *hw, unsigned long rate); + +#endif /* _WM_FLL_H */