| // 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"); |