]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ASoC: cs40l50: Support I2S streaming to CS40L50
authorJames Ogletree <jogletre@opensource.cirrus.com>
Thu, 20 Jun 2024 16:17:45 +0000 (16:17 +0000)
committerLee Jones <lee@kernel.org>
Fri, 28 Jun 2024 14:36:15 +0000 (15:36 +0100)
Introduce support for Cirrus Logic Device CS40L50: a
haptic driver with waveform memory, integrated DSP,
and closed-loop algorithms.

The ASoC driver enables I2S streaming to the device.

Reviewed-by: David Rhodes <drhodes@opensource.cirrus.com>
Signed-off-by: James Ogletree <jogletre@opensource.cirrus.com>
Reviewed-by: Jeff LaBundy <jeff@labundy.com>
Reviewed-by: Ricardo Rivera-Matos <rriveram@opensource.cirrus.com>
Reviewed-by: Mark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20240620161745.2312359-6-jogletre@opensource.cirrus.com
Signed-off-by: Lee Jones <lee@kernel.org>
MAINTAINERS
sound/soc/codecs/Kconfig
sound/soc/codecs/Makefile
sound/soc/codecs/cs40l50-codec.c [new file with mode: 0644]

index 2cc63d886e592816c4efd69075e144603886d5c6..5932228347a3205f4ce49a814f9edfa0ad5fe566 100644 (file)
@@ -5216,6 +5216,7 @@ F:        Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml
 F:     drivers/input/misc/cs40l*
 F:     drivers/mfd/cs40l*
 F:     include/linux/mfd/cs40l*
+F:     sound/soc/codecs/cs40l*
 
 CIRRUS LOGIC DSP FIRMWARE DRIVER
 M:     Simon Trimmer <simont@opensource.cirrus.com>
index 4afc43d3f71fd71fc440b915d6656154f5ddda14..91b502f8cc3f8a53d329daf78b2e64a665b6608d 100644 (file)
@@ -75,6 +75,7 @@ config SND_SOC_ALL_CODECS
        imply SND_SOC_CS35L56_I2C
        imply SND_SOC_CS35L56_SPI
        imply SND_SOC_CS35L56_SDW
+       imply SND_SOC_CS40L50
        imply SND_SOC_CS42L42
        imply SND_SOC_CS42L42_SDW
        imply SND_SOC_CS42L43
@@ -847,6 +848,16 @@ config SND_SOC_CS35L56_SDW
        help
          Enable support for Cirrus Logic CS35L56 boosted amplifier with SoundWire control
 
+config SND_SOC_CS40L50
+       tristate "Cirrus Logic CS40L50 CODEC"
+       depends on MFD_CS40L50_CORE
+       help
+         This option enables support for I2S streaming to Cirrus Logic CS40L50.
+
+         CS40L50 is a haptic driver with waveform memory, an integrated
+         DSP, and closed-loop algorithms. If built as a module, it will be
+         called snd-soc-cs40l50.
+
 config SND_SOC_CS42L42_CORE
        tristate
 
index b4df22186e2552f4d96d7dfc77c83f131c64e916..3afd7c16c95968c3eb78394b80b34dc4b249abca 100644 (file)
@@ -78,6 +78,7 @@ snd-soc-cs35l56-shared-y := cs35l56-shared.o
 snd-soc-cs35l56-i2c-y := cs35l56-i2c.o
 snd-soc-cs35l56-spi-y := cs35l56-spi.o
 snd-soc-cs35l56-sdw-y := cs35l56-sdw.o
+snd-soc-cs40l50-objs := cs40l50-codec.o
 snd-soc-cs42l42-y := cs42l42.o
 snd-soc-cs42l42-i2c-y := cs42l42-i2c.o
 snd-soc-cs42l42-sdw-y := cs42l42-sdw.o
@@ -475,6 +476,7 @@ obj-$(CONFIG_SND_SOC_CS35L56_SHARED)        += snd-soc-cs35l56-shared.o
 obj-$(CONFIG_SND_SOC_CS35L56_I2C)      += snd-soc-cs35l56-i2c.o
 obj-$(CONFIG_SND_SOC_CS35L56_SPI)      += snd-soc-cs35l56-spi.o
 obj-$(CONFIG_SND_SOC_CS35L56_SDW)      += snd-soc-cs35l56-sdw.o
+obj-$(CONFIG_SND_SOC_CS40L50)          += snd-soc-cs40l50.o
 obj-$(CONFIG_SND_SOC_CS42L42_CORE)     += snd-soc-cs42l42.o
 obj-$(CONFIG_SND_SOC_CS42L42)  += snd-soc-cs42l42-i2c.o
 obj-$(CONFIG_SND_SOC_CS42L42_SDW)      += snd-soc-cs42l42-sdw.o
diff --git a/sound/soc/codecs/cs40l50-codec.c b/sound/soc/codecs/cs40l50-codec.c
new file mode 100644 (file)
index 0000000..aa629ef
--- /dev/null
@@ -0,0 +1,307 @@
+// 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");