Unverified Commit 4235c80b authored by Mark Brown's avatar Mark Brown
Browse files

Add audio support for LPC32XX CPUs

Merge series from Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>:

This pach set is to bring back audio to machines with a LPC32XX CPU.
The legacy LPC32XX SoC used to have audio spport in linux 2.6.27.
The support was dropped due to lack of interest from mainaeners.
parents 730674b2 0959de65
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/sound/nxp,lpc3220-i2s.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: NXP LPC32XX I2S Controller

description:
  The I2S controller in LPC32XX SoCs, ASoC DAI.

maintainers:
  - J.M.B. Downing <jonathan.downing@nautel.com>
  - Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>

allOf:
  - $ref: dai-common.yaml#

properties:
  compatible:
    enum:
      - nxp,lpc3220-i2s

  reg:
    maxItems: 1

  interrupts:
    maxItems: 1

  clocks:
    items:
      - description: input clock of the peripheral.

  dmas:
    items:
      - description: RX DMA Channel
      - description: TX DMA Channel

  dma-names:
    items:
      - const: rx
      - const: tx

  "#sound-dai-cells":
    const: 0

required:
  - compatible
  - reg
  - interrupts
  - clocks
  - dmas
  - dma-names
  - '#sound-dai-cells'

additionalProperties: false

examples:
  - |
    #include <dt-bindings/clock/lpc32xx-clock.h>
    #include <dt-bindings/interrupt-controller/irq.h>

    i2s@20094000 {
      compatible = "nxp,lpc3220-i2s";
      reg = <0x20094000 0x1000>;
      interrupts = <22 IRQ_TYPE_LEVEL_HIGH>;
      clocks = <&clk LPC32XX_CLK_I2S0>;
      dmas = <&dma 0 1>, <&dma 13 1>;
      dma-names = "rx", "tx";
      #sound-dai-cells = <0>;
    };

...
+10 −0
Original line number Diff line number Diff line
@@ -8909,6 +8909,16 @@ S: Maintained
F:	sound/soc/fsl/fsl*
F:	sound/soc/fsl/imx*
FREESCALE SOC LPC32XX SOUND DRIVERS
M:	J.M.B. Downing <jonathan.downing@nautel.com>
M:	Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>
R:	Vladimir Zapolskiy <vz@mleia.com>
L:	alsa-devel@alsa-project.org (moderated for non-subscribers)
L:	linuxppc-dev@lists.ozlabs.org
S:	Maintained
F:	Documentation/devicetree/bindings/sound/nxp,lpc3220-i2s.yaml
F:	sound/soc/fsl/lpc3xxx-*
FREESCALE SOC SOUND QMC DRIVER
M:	Herve Codina <herve.codina@bootlin.com>
L:	alsa-devel@alsa-project.org (moderated for non-subscribers)
+7 −0
Original line number Diff line number Diff line
@@ -131,6 +131,13 @@ config SND_SOC_FSL_RPMSG
	  This option is only useful for out-of-tree drivers since
	  in-tree drivers select it automatically.

config SND_SOC_FSL_LPC3XXX
	tristate "SoC Audio for NXP LPC32XX CPUs"
	depends on ARCH_LPC32XX || COMPILE_TEST
	select SND_SOC_GENERIC_DMAENGINE_PCM
	help
	  Say Y or M if you want to add support for the LPC3XXX I2S interface.

config SND_SOC_IMX_PCM_DMA
	tristate
	select SND_SOC_GENERIC_DMAENGINE_PCM
+2 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ obj-$(CONFIG_SND_SOC_P1022_RDK) += snd-soc-p1022-rdk.o
snd-soc-fsl-audmix-y := fsl_audmix.o
snd-soc-fsl-asoc-card-y := fsl-asoc-card.o
snd-soc-fsl-asrc-y := fsl_asrc.o fsl_asrc_dma.o
snd-soc-fsl-lpc3xxx-y := lpc3xxx-pcm.o lpc3xxx-i2s.o
snd-soc-fsl-sai-y := fsl_sai.o
snd-soc-fsl-ssi-y := fsl_ssi.o
snd-soc-fsl-ssi-$(CONFIG_DEBUG_FS) += fsl_ssi_dbg.o
@@ -29,6 +30,7 @@ snd-soc-fsl-qmc-audio-y := fsl_qmc_audio.o
obj-$(CONFIG_SND_SOC_FSL_AUDMIX) += snd-soc-fsl-audmix.o
obj-$(CONFIG_SND_SOC_FSL_ASOC_CARD) += snd-soc-fsl-asoc-card.o
obj-$(CONFIG_SND_SOC_FSL_ASRC) += snd-soc-fsl-asrc.o
obj-$(CONFIG_SND_SOC_FSL_LPC3XXX) += snd-soc-fsl-lpc3xxx.o
obj-$(CONFIG_SND_SOC_FSL_SAI) += snd-soc-fsl-sai.o
obj-$(CONFIG_SND_SOC_FSL_SSI) += snd-soc-fsl-ssi.o
obj-$(CONFIG_SND_SOC_FSL_SPDIF) += snd-soc-fsl-spdif.o
+375 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Author: Kevin Wells <kevin.wells@nxp.com>
//
// Copyright (C) 2008 NXP Semiconductors
// Copyright 2023 Timesys Corporation <piotr.wojtaszczyk@timesys.com>

#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/io.h>

#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/dmaengine_pcm.h>
#include <sound/initval.h>
#include <sound/soc.h>

#include "lpc3xxx-i2s.h"

#define I2S_PLAYBACK_FLAG 0x1
#define I2S_CAPTURE_FLAG 0x2

#define LPC3XXX_I2S_RATES ( \
	SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | \
	SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 | \
	SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000)

#define LPC3XXX_I2S_FORMATS ( \
	SNDRV_PCM_FMTBIT_S8 | \
	SNDRV_PCM_FMTBIT_S16_LE | \
	SNDRV_PCM_FMTBIT_S32_LE)

static void __lpc3xxx_find_clkdiv(u32 *clkx, u32 *clky, int freq, int xbytes, u32 clkrate)
{
	u32 i2srate;
	u32 idxx, idyy;
	u32 savedbitclkrate, diff, trate, baseclk;

	/* Adjust rate for sample size (bits) and 2 channels and offset for
	 * divider in clock output
	 */
	i2srate = (freq / 100) * 2 * (8 * xbytes);
	i2srate = i2srate << 1;
	clkrate = clkrate / 100;
	baseclk = clkrate;
	*clkx = 1;
	*clky = 1;

	/* Find the best divider */
	*clkx = *clky = 0;
	savedbitclkrate = 0;
	diff = ~0;
	for (idxx = 1; idxx < 0xFF; idxx++) {
		for (idyy = 1; idyy < 0xFF; idyy++) {
			trate = (baseclk * idxx) / idyy;
			if (abs(trate - i2srate) < diff) {
				diff = abs(trate - i2srate);
				savedbitclkrate = trate;
				*clkx = idxx;
				*clky = idyy;
			}
		}
	}
}

static int lpc3xxx_i2s_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *cpu_dai)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);
	struct device *dev = i2s_info_p->dev;
	u32 flag;
	int ret = 0;

	guard(mutex)(&i2s_info_p->lock);

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		flag = I2S_PLAYBACK_FLAG;
	else
		flag = I2S_CAPTURE_FLAG;

	if (flag & i2s_info_p->streams_in_use) {
		dev_warn(dev, "I2S channel is busy\n");
		ret = -EBUSY;
		return ret;
	}

	if (i2s_info_p->streams_in_use == 0) {
		ret = clk_prepare_enable(i2s_info_p->clk);
		if (ret) {
			dev_err(dev, "Can't enable clock, err=%d\n", ret);
			return ret;
		}
	}

	i2s_info_p->streams_in_use |= flag;
	return 0;
}

static void lpc3xxx_i2s_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *cpu_dai)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);
	struct regmap *regs = i2s_info_p->regs;
	const u32 stop_bits = (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP);
	u32 flag;

	guard(mutex)(&i2s_info_p->lock);

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
		flag = I2S_PLAYBACK_FLAG;
		regmap_write(regs, LPC3XXX_REG_I2S_TX_RATE, 0);
		regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO, stop_bits, stop_bits);
	} else {
		flag = I2S_CAPTURE_FLAG;
		regmap_write(regs, LPC3XXX_REG_I2S_RX_RATE, 0);
		regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI, stop_bits, stop_bits);
	}
	i2s_info_p->streams_in_use &= ~flag;

	if (i2s_info_p->streams_in_use == 0)
		clk_disable_unprepare(i2s_info_p->clk);
}

static int lpc3xxx_i2s_set_dai_sysclk(struct snd_soc_dai *cpu_dai,
				      int clk_id, unsigned int freq, int dir)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);

	/* Will use in HW params later */
	i2s_info_p->freq = freq;

	return 0;
}

static int lpc3xxx_i2s_set_dai_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);
	struct device *dev = i2s_info_p->dev;

	if ((fmt & SND_SOC_DAIFMT_FORMAT_MASK) != SND_SOC_DAIFMT_I2S) {
		dev_warn(dev, "unsupported bus format %d\n", fmt);
		return -EINVAL;
	}

	if ((fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) != SND_SOC_DAIFMT_BP_FP) {
		dev_warn(dev, "unsupported clock provider %d\n", fmt);
		return -EINVAL;
	}

	return 0;
}

static int lpc3xxx_i2s_hw_params(struct snd_pcm_substream *substream,
				 struct snd_pcm_hw_params *params,
				 struct snd_soc_dai *cpu_dai)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);
	struct device *dev = i2s_info_p->dev;
	struct regmap *regs = i2s_info_p->regs;
	int xfersize;
	u32 tmp, clkx, clky;

	tmp = LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP;
	switch (params_format(params)) {
	case SNDRV_PCM_FORMAT_S8:
		tmp |= LPC3XXX_I2S_WW8 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW8_HP);
		xfersize = 1;
		break;

	case SNDRV_PCM_FORMAT_S16_LE:
		tmp |= LPC3XXX_I2S_WW16 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW16_HP);
		xfersize = 2;
		break;

	case SNDRV_PCM_FORMAT_S32_LE:
		tmp |= LPC3XXX_I2S_WW32 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW32_HP);
		xfersize = 4;
		break;

	default:
		dev_warn(dev, "Unsupported audio data format %d\n", params_format(params));
		return -EINVAL;
	}

	if (params_channels(params) == 1)
		tmp |= LPC3XXX_I2S_MONO;

	__lpc3xxx_find_clkdiv(&clkx, &clky, i2s_info_p->freq, xfersize, i2s_info_p->clkrate);

	dev_dbg(dev, "Stream                : %s\n",
		substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? "playback" : "capture");
	dev_dbg(dev, "Desired clock rate    : %d\n", i2s_info_p->freq);
	dev_dbg(dev, "Base clock rate       : %d\n", i2s_info_p->clkrate);
	dev_dbg(dev, "Transfer size (bytes) : %d\n", xfersize);
	dev_dbg(dev, "Clock divider (x)     : %d\n", clkx);
	dev_dbg(dev, "Clock divider (y)     : %d\n", clky);
	dev_dbg(dev, "Channels              : %d\n", params_channels(params));
	dev_dbg(dev, "Data format           : %s\n", "I2S");

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
		regmap_write(regs, LPC3XXX_REG_I2S_DMA1,
			     LPC3XXX_I2S_DMA1_TX_EN | LPC3XXX_I2S_DMA0_TX_DEPTH(4));
		regmap_write(regs, LPC3XXX_REG_I2S_TX_RATE, (clkx << 8) | clky);
		regmap_write(regs, LPC3XXX_REG_I2S_DAO, tmp);
	} else {
		regmap_write(regs, LPC3XXX_REG_I2S_DMA0,
			     LPC3XXX_I2S_DMA0_RX_EN | LPC3XXX_I2S_DMA1_RX_DEPTH(4));
		regmap_write(regs, LPC3XXX_REG_I2S_RX_RATE, (clkx << 8) | clky);
		regmap_write(regs, LPC3XXX_REG_I2S_DAI, tmp);
	}

	return 0;
}

static int lpc3xxx_i2s_trigger(struct snd_pcm_substream *substream, int cmd,
			       struct snd_soc_dai *cpu_dai)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai);
	struct regmap *regs = i2s_info_p->regs;
	int ret = 0;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
			regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO,
					   LPC3XXX_I2S_STOP, LPC3XXX_I2S_STOP);
		else
			regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI,
					   LPC3XXX_I2S_STOP, LPC3XXX_I2S_STOP);
		break;

	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
	case SNDRV_PCM_TRIGGER_RESUME:
		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
			regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO,
					   (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP), 0);
		else
			regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI,
					   (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP), 0);
		break;
	default:
		ret = -EINVAL;
	}

	return ret;
}

static int lpc3xxx_i2s_dai_probe(struct snd_soc_dai *dai)
{
	struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(dai);

	snd_soc_dai_init_dma_data(dai, &i2s_info_p->playback_dma_config,
				  &i2s_info_p->capture_dma_config);
	return 0;
}

const struct snd_soc_dai_ops lpc3xxx_i2s_dai_ops = {
	.probe	= lpc3xxx_i2s_dai_probe,
	.startup = lpc3xxx_i2s_startup,
	.shutdown = lpc3xxx_i2s_shutdown,
	.trigger = lpc3xxx_i2s_trigger,
	.hw_params = lpc3xxx_i2s_hw_params,
	.set_sysclk = lpc3xxx_i2s_set_dai_sysclk,
	.set_fmt = lpc3xxx_i2s_set_dai_fmt,
};

struct snd_soc_dai_driver lpc3xxx_i2s_dai_driver = {
	.playback = {
		.channels_min = 1,
		.channels_max = 2,
		.rates = LPC3XXX_I2S_RATES,
		.formats = LPC3XXX_I2S_FORMATS,
		},
	.capture = {
		.channels_min = 1,
		.channels_max = 2,
		.rates = LPC3XXX_I2S_RATES,
		.formats = LPC3XXX_I2S_FORMATS,
		},
	.ops = &lpc3xxx_i2s_dai_ops,
	.symmetric_rate = 1,
	.symmetric_channels = 1,
	.symmetric_sample_bits = 1,
};

static const struct snd_soc_component_driver lpc32xx_i2s_component = {
	.name = "lpc32xx-i2s",
	.legacy_dai_naming = 1,
};

static const struct regmap_config lpc32xx_i2s_regconfig = {
	.reg_bits = 32,
	.reg_stride = 4,
	.val_bits = 32,
	.max_register = LPC3XXX_REG_I2S_RX_RATE,
};

static int lpc32xx_i2s_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct lpc3xxx_i2s_info *i2s_info_p;
	struct resource *res;
	void __iomem *iomem;
	int ret;

	i2s_info_p = devm_kzalloc(dev, sizeof(*i2s_info_p), GFP_KERNEL);
	if (!i2s_info_p)
		return -ENOMEM;

	platform_set_drvdata(pdev, i2s_info_p);
	i2s_info_p->dev = dev;

	iomem = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
	if (IS_ERR(iomem))
		return dev_err_probe(dev, PTR_ERR(iomem), "Can't map registers\n");

	i2s_info_p->regs = devm_regmap_init_mmio(dev, iomem, &lpc32xx_i2s_regconfig);
	if (IS_ERR(i2s_info_p->regs))
		return dev_err_probe(dev, PTR_ERR(i2s_info_p->regs),
				     "failed to init register map: %d\n", ret);

	i2s_info_p->clk = devm_clk_get(dev, NULL);
	if (IS_ERR(i2s_info_p->clk))
		return dev_err_probe(dev, PTR_ERR(i2s_info_p->clk), "Can't get clock\n");

	i2s_info_p->clkrate = clk_get_rate(i2s_info_p->clk);
	if (i2s_info_p->clkrate == 0)
		return dev_err_probe(dev, -EINVAL, "Invalid returned clock rate\n");

	mutex_init(&i2s_info_p->lock);

	ret = devm_snd_soc_register_component(dev, &lpc32xx_i2s_component,
					      &lpc3xxx_i2s_dai_driver, 1);
	if (ret)
		return dev_err_probe(dev, ret, "Can't register cpu_dai component\n");

	i2s_info_p->playback_dma_config.addr = (dma_addr_t)(res->start + LPC3XXX_REG_I2S_TX_FIFO);
	i2s_info_p->playback_dma_config.maxburst = 4;

	i2s_info_p->capture_dma_config.addr = (dma_addr_t)(res->start + LPC3XXX_REG_I2S_RX_FIFO);
	i2s_info_p->capture_dma_config.maxburst = 4;

	ret = lpc3xxx_pcm_register(pdev);
	if (ret)
		return dev_err_probe(dev, ret, "Can't register pcm component\n");

	return 0;
}

static const struct of_device_id lpc32xx_i2s_match[] = {
	{ .compatible = "nxp,lpc3220-i2s" },
	{},
};
MODULE_DEVICE_TABLE(of, lpc32xx_i2s_match);

static struct platform_driver lpc32xx_i2s_driver = {
	.probe = lpc32xx_i2s_probe,
	.driver		= {
		.name	= "lpc3xxx-i2s",
		.of_match_table = lpc32xx_i2s_match,
	},
};

module_platform_driver(lpc32xx_i2s_driver);

MODULE_AUTHOR("Kevin Wells <kevin.wells@nxp.com>");
MODULE_AUTHOR("Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>");
MODULE_DESCRIPTION("ASoC LPC3XXX I2S interface");
MODULE_LICENSE("GPL");
Loading