|  | // SPDX-License-Identifier: GPL-2.0 | 
|  | // | 
|  | // CS40L50 Advanced Haptic Driver with waveform memory, | 
|  | // integrated DSP, and closed-loop algorithms | 
|  | // | 
|  | // Copyright 2024 Cirrus Logic, Inc. | 
|  | // | 
|  | // Author: James Ogletree <james.ogletree@cirrus.com> | 
|  |  | 
|  | #include <linux/bitfield.h> | 
|  | #include <linux/mfd/cs40l50.h> | 
|  | #include <sound/pcm_params.h> | 
|  | #include <sound/soc.h> | 
|  |  | 
|  | #define CS40L50_REFCLK_INPUT		0x2C04 | 
|  | #define CS40L50_ASP_CONTROL2		0x4808 | 
|  | #define CS40L50_ASP_DATA_CONTROL5	0x4840 | 
|  |  | 
|  | /* PLL Config */ | 
|  | #define CS40L50_PLL_REFCLK_BCLK		0x0 | 
|  | #define CS40L50_PLL_REFCLK_MCLK		0x5 | 
|  | #define CS40L50_PLL_REEFCLK_MCLK_CFG	0x00 | 
|  | #define CS40L50_PLL_REFCLK_LOOP_MASK	BIT(11) | 
|  | #define CS40L50_PLL_REFCLK_OPEN_LOOP	1 | 
|  | #define CS40L50_PLL_REFCLK_CLOSED_LOOP	0 | 
|  | #define CS40L50_PLL_REFCLK_LOOP_SHIFT	11 | 
|  | #define CS40L50_PLL_REFCLK_FREQ_MASK	GENMASK(10, 5) | 
|  | #define CS40L50_PLL_REFCLK_FREQ_SHIFT	5 | 
|  | #define CS40L50_PLL_REFCLK_SEL_MASK	GENMASK(2, 0) | 
|  | #define CS40L50_BCLK_RATIO_DEFAULT	32 | 
|  |  | 
|  | /* ASP Config */ | 
|  | #define CS40L50_ASP_RX_WIDTH_SHIFT	24 | 
|  | #define CS40L50_ASP_RX_WIDTH_MASK	GENMASK(31, 24) | 
|  | #define CS40L50_ASP_RX_WL_MASK		GENMASK(5, 0) | 
|  | #define CS40L50_ASP_FSYNC_INV_MASK	BIT(2) | 
|  | #define CS40L50_ASP_BCLK_INV_MASK	BIT(6) | 
|  | #define CS40L50_ASP_FMT_MASK		GENMASK(10, 8) | 
|  | #define CS40L50_ASP_FMT_I2S		0x2 | 
|  |  | 
|  | struct cs40l50_pll_config { | 
|  | unsigned int freq; | 
|  | unsigned int cfg; | 
|  | }; | 
|  |  | 
|  | struct cs40l50_codec { | 
|  | struct device *dev; | 
|  | struct regmap *regmap; | 
|  | unsigned int daifmt; | 
|  | unsigned int bclk_ratio; | 
|  | unsigned int rate; | 
|  | }; | 
|  |  | 
|  | static const struct cs40l50_pll_config cs40l50_pll_cfg[] = { | 
|  | { 32768, 0x00 }, | 
|  | { 1536000, 0x1B }, | 
|  | { 3072000, 0x21 }, | 
|  | { 6144000, 0x28 }, | 
|  | { 9600000, 0x30 }, | 
|  | { 12288000, 0x33 }, | 
|  | }; | 
|  |  | 
|  | static int cs40l50_get_clk_config(const unsigned int freq, unsigned int *cfg) | 
|  | { | 
|  | int i; | 
|  |  | 
|  | for (i = 0; i < ARRAY_SIZE(cs40l50_pll_cfg); i++) { | 
|  | if (cs40l50_pll_cfg[i].freq == freq) { | 
|  | *cfg = cs40l50_pll_cfg[i].cfg; | 
|  | return 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | static int cs40l50_swap_ext_clk(struct cs40l50_codec *codec, const unsigned int clk_src) | 
|  | { | 
|  | unsigned int cfg; | 
|  | int ret; | 
|  |  | 
|  | switch (clk_src) { | 
|  | case CS40L50_PLL_REFCLK_BCLK: | 
|  | ret = cs40l50_get_clk_config(codec->bclk_ratio * codec->rate, &cfg); | 
|  | if (ret) | 
|  | return ret; | 
|  | break; | 
|  | case CS40L50_PLL_REFCLK_MCLK: | 
|  | cfg = CS40L50_PLL_REEFCLK_MCLK_CFG; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | ret = regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT, | 
|  | CS40L50_PLL_REFCLK_LOOP_MASK, | 
|  | CS40L50_PLL_REFCLK_OPEN_LOOP << | 
|  | CS40L50_PLL_REFCLK_LOOP_SHIFT); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | ret = regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT, | 
|  | CS40L50_PLL_REFCLK_FREQ_MASK | | 
|  | CS40L50_PLL_REFCLK_SEL_MASK, | 
|  | (cfg << CS40L50_PLL_REFCLK_FREQ_SHIFT) | clk_src); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | return regmap_update_bits(codec->regmap, CS40L50_REFCLK_INPUT, | 
|  | CS40L50_PLL_REFCLK_LOOP_MASK, | 
|  | CS40L50_PLL_REFCLK_CLOSED_LOOP << | 
|  | CS40L50_PLL_REFCLK_LOOP_SHIFT); | 
|  | } | 
|  |  | 
|  | static int cs40l50_clk_en(struct snd_soc_dapm_widget *w, | 
|  | struct snd_kcontrol *kcontrol, | 
|  | int event) | 
|  | { | 
|  | struct snd_soc_component *comp = snd_soc_dapm_to_component(w->dapm); | 
|  | struct cs40l50_codec *codec = snd_soc_component_get_drvdata(comp); | 
|  | int ret; | 
|  |  | 
|  | switch (event) { | 
|  | case SND_SOC_DAPM_POST_PMU: | 
|  | ret = cs40l50_dsp_write(codec->dev, codec->regmap, CS40L50_STOP_PLAYBACK); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | ret = cs40l50_dsp_write(codec->dev, codec->regmap, CS40L50_START_I2S); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | ret = cs40l50_swap_ext_clk(codec, CS40L50_PLL_REFCLK_BCLK); | 
|  | if (ret) | 
|  | return ret; | 
|  | break; | 
|  | case SND_SOC_DAPM_PRE_PMD: | 
|  | ret = cs40l50_swap_ext_clk(codec, CS40L50_PLL_REFCLK_MCLK); | 
|  | if (ret) | 
|  | return ret; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct snd_soc_dapm_widget cs40l50_dapm_widgets[] = { | 
|  | SND_SOC_DAPM_SUPPLY_S("ASP PLL", 0, SND_SOC_NOPM, 0, 0, cs40l50_clk_en, | 
|  | SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), | 
|  | SND_SOC_DAPM_AIF_IN("ASPRX1", NULL, 0, SND_SOC_NOPM, 0, 0), | 
|  | SND_SOC_DAPM_AIF_IN("ASPRX2", NULL, 0, SND_SOC_NOPM, 0, 0), | 
|  | SND_SOC_DAPM_OUTPUT("OUT"), | 
|  | }; | 
|  |  | 
|  | static const struct snd_soc_dapm_route cs40l50_dapm_routes[] = { | 
|  | { "ASP Playback", NULL, "ASP PLL" }, | 
|  | { "ASPRX1", NULL, "ASP Playback" }, | 
|  | { "ASPRX2", NULL, "ASP Playback" }, | 
|  |  | 
|  | { "OUT", NULL, "ASPRX1" }, | 
|  | { "OUT", NULL, "ASPRX2" }, | 
|  | }; | 
|  |  | 
|  | static int cs40l50_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt) | 
|  | { | 
|  | struct cs40l50_codec *codec = snd_soc_component_get_drvdata(codec_dai->component); | 
|  |  | 
|  | if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBC_CFC) | 
|  | return -EINVAL; | 
|  |  | 
|  | switch (fmt & SND_SOC_DAIFMT_INV_MASK) { | 
|  | case SND_SOC_DAIFMT_NB_NF: | 
|  | codec->daifmt = 0; | 
|  | break; | 
|  | case SND_SOC_DAIFMT_NB_IF: | 
|  | codec->daifmt = CS40L50_ASP_FSYNC_INV_MASK; | 
|  | break; | 
|  | case SND_SOC_DAIFMT_IB_NF: | 
|  | codec->daifmt = CS40L50_ASP_BCLK_INV_MASK; | 
|  | break; | 
|  | case SND_SOC_DAIFMT_IB_IF: | 
|  | codec->daifmt = CS40L50_ASP_FSYNC_INV_MASK | CS40L50_ASP_BCLK_INV_MASK; | 
|  | break; | 
|  | default: | 
|  | dev_err(codec->dev, "Invalid clock invert\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { | 
|  | case SND_SOC_DAIFMT_I2S: | 
|  | codec->daifmt |= FIELD_PREP(CS40L50_ASP_FMT_MASK, CS40L50_ASP_FMT_I2S); | 
|  | break; | 
|  | default: | 
|  | dev_err(codec->dev, "Unsupported DAI format\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int cs40l50_hw_params(struct snd_pcm_substream *substream, | 
|  | struct snd_pcm_hw_params *params, | 
|  | struct snd_soc_dai *dai) | 
|  | { | 
|  | struct cs40l50_codec *codec = snd_soc_component_get_drvdata(dai->component); | 
|  | unsigned int asp_rx_wl = params_width(params); | 
|  | int ret; | 
|  |  | 
|  | codec->rate = params_rate(params); | 
|  |  | 
|  | ret = regmap_update_bits(codec->regmap, CS40L50_ASP_DATA_CONTROL5, | 
|  | CS40L50_ASP_RX_WL_MASK, asp_rx_wl); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | codec->daifmt |= (asp_rx_wl << CS40L50_ASP_RX_WIDTH_SHIFT); | 
|  |  | 
|  | return regmap_update_bits(codec->regmap, CS40L50_ASP_CONTROL2, | 
|  | CS40L50_ASP_FSYNC_INV_MASK | | 
|  | CS40L50_ASP_BCLK_INV_MASK | | 
|  | CS40L50_ASP_FMT_MASK | | 
|  | CS40L50_ASP_RX_WIDTH_MASK, codec->daifmt); | 
|  | } | 
|  |  | 
|  | static int cs40l50_set_dai_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio) | 
|  | { | 
|  | struct cs40l50_codec *codec = snd_soc_component_get_drvdata(dai->component); | 
|  |  | 
|  | codec->bclk_ratio = ratio; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct snd_soc_dai_ops cs40l50_dai_ops = { | 
|  | .set_fmt = cs40l50_set_dai_fmt, | 
|  | .set_bclk_ratio = cs40l50_set_dai_bclk_ratio, | 
|  | .hw_params = cs40l50_hw_params, | 
|  | }; | 
|  |  | 
|  | static struct snd_soc_dai_driver cs40l50_dai[] = { | 
|  | { | 
|  | .name = "cs40l50-pcm", | 
|  | .id = 0, | 
|  | .playback = { | 
|  | .stream_name = "ASP Playback", | 
|  | .channels_min = 1, | 
|  | .channels_max = 2, | 
|  | .rates = SNDRV_PCM_RATE_48000, | 
|  | .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE, | 
|  | }, | 
|  | .ops = &cs40l50_dai_ops, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | static int cs40l50_codec_probe(struct snd_soc_component *component) | 
|  | { | 
|  | struct cs40l50_codec *codec = snd_soc_component_get_drvdata(component); | 
|  |  | 
|  | codec->bclk_ratio = CS40L50_BCLK_RATIO_DEFAULT; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct snd_soc_component_driver soc_codec_dev_cs40l50 = { | 
|  | .probe = cs40l50_codec_probe, | 
|  | .dapm_widgets = cs40l50_dapm_widgets, | 
|  | .num_dapm_widgets = ARRAY_SIZE(cs40l50_dapm_widgets), | 
|  | .dapm_routes = cs40l50_dapm_routes, | 
|  | .num_dapm_routes = ARRAY_SIZE(cs40l50_dapm_routes), | 
|  | }; | 
|  |  | 
|  | static int cs40l50_codec_driver_probe(struct platform_device *pdev) | 
|  | { | 
|  | struct cs40l50 *cs40l50 = dev_get_drvdata(pdev->dev.parent); | 
|  | struct cs40l50_codec *codec; | 
|  |  | 
|  | codec = devm_kzalloc(&pdev->dev, sizeof(*codec), GFP_KERNEL); | 
|  | if (!codec) | 
|  | return -ENOMEM; | 
|  |  | 
|  | codec->regmap = cs40l50->regmap; | 
|  | codec->dev = &pdev->dev; | 
|  |  | 
|  | return devm_snd_soc_register_component(&pdev->dev, &soc_codec_dev_cs40l50, | 
|  | cs40l50_dai, ARRAY_SIZE(cs40l50_dai)); | 
|  | } | 
|  |  | 
|  | static const struct platform_device_id cs40l50_id[] = { | 
|  | { "cs40l50-codec", }, | 
|  | {} | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(platform, cs40l50_id); | 
|  |  | 
|  | static struct platform_driver cs40l50_codec_driver = { | 
|  | .probe = cs40l50_codec_driver_probe, | 
|  | .id_table = cs40l50_id, | 
|  | .driver = { | 
|  | .name = "cs40l50-codec", | 
|  | }, | 
|  | }; | 
|  | module_platform_driver(cs40l50_codec_driver); | 
|  |  | 
|  | MODULE_DESCRIPTION("ASoC CS40L50 driver"); | 
|  | MODULE_AUTHOR("James Ogletree <james.ogletree@cirrus.com>"); | 
|  | MODULE_LICENSE("GPL"); |