Commit c486def5 authored by James Ogletree's avatar James Ogletree Committed by Lee Jones
Browse files

ASoC: cs40l50: Support I2S streaming to CS40L50



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: default avatarDavid Rhodes <drhodes@opensource.cirrus.com>
Signed-off-by: default avatarJames Ogletree <jogletre@opensource.cirrus.com>
Reviewed-by: default avatarJeff LaBundy <jeff@labundy.com>
Reviewed-by: default avatarRicardo Rivera-Matos <rriveram@opensource.cirrus.com>
Reviewed-by: default avatarMark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20240620161745.2312359-6-jogletre@opensource.cirrus.com


Signed-off-by: default avatarLee Jones <lee@kernel.org>
parent c38fe1bb
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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>
+11 −0
Original line number Diff line number Diff line
@@ -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

+2 −0
Original line number Diff line number Diff line
@@ -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
+307 −0
Original line number Diff line number Diff line
// 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");