From: Codrin Ciubotariu codrin.ciubotariu@microchip.com
The Inter-IC Sound Controller (I2SMCC) provides a 5-wire, bidirectional, synchronous, digital audio link to external audio devices: I2SMCC_DIN, I2SMCC_DOUT, I2SMCC_WS, I2SMCC_CK, and I2SMCC_MCK pins. The I2SMCC complies with the Inter-IC Sound (I2S) bus specification and supports a Time Division Multiplexed (TDM) interface with external multi-channel audio codecs. The I2SMCC consists of a receiver, a transmitter and a common clock generator that can be enabled separately to provide Master, Slave or Controller modes with receiver and/or transmitter active. DMA Controller channels, separate for the receiver and for the transmitter, allow a continuous high bit rate data transfer without processor intervention to the following: - Audio CODECs in Master, Slave, or Controller mode - Stereo DAC or ADC through a dedicated I2S serial interface - Multi-channel or multiple stereo DACs or ADCs, using the TDM format
This IP is embedded in Microchip's new sam9x60 SoC.
Signed-off-by: Codrin Ciubotariu codrin.ciubotariu@microchip.com --- sound/soc/atmel/Kconfig | 14 + sound/soc/atmel/Makefile | 2 + sound/soc/atmel/mchp-i2s-mcc.c | 974 +++++++++++++++++++++++++++++++++ 3 files changed, 990 insertions(+) create mode 100644 sound/soc/atmel/mchp-i2s-mcc.c
diff --git a/sound/soc/atmel/Kconfig b/sound/soc/atmel/Kconfig index 64f86f0b87e5..c473b9e463ab 100644 --- a/sound/soc/atmel/Kconfig +++ b/sound/soc/atmel/Kconfig @@ -109,4 +109,18 @@ config SND_SOC_MIKROE_PROTO using I2C over SDA (MPU Data Input) and SCL (MPU Clock Input) pins. Both playback and capture are supported.
+config SND_MCHP_SOC_I2S_MCC + tristate "Microchip ASoC driver for boards using I2S MCC" + depends on OF && (ARCH_AT91 || COMPILE_TEST) + select SND_SOC_GENERIC_DMAENGINE_PCM + select REGMAP_MMIO + help + Say Y or M if you want to add support for I2S Multi-Channel ASoC + driver on the following Microchip platforms: + - sam9x60 + + The I2SMCC complies with the Inter-IC Sound (I2S) bus specification + and supports a Time Division Multiplexed (TDM) interface with + external multi-channel audio codecs. + endif diff --git a/sound/soc/atmel/Makefile b/sound/soc/atmel/Makefile index 9f41bfa0fea3..1f6890ed3738 100644 --- a/sound/soc/atmel/Makefile +++ b/sound/soc/atmel/Makefile @@ -4,11 +4,13 @@ snd-soc-atmel-pcm-pdc-objs := atmel-pcm-pdc.o snd-soc-atmel-pcm-dma-objs := atmel-pcm-dma.o snd-soc-atmel_ssc_dai-objs := atmel_ssc_dai.o snd-soc-atmel-i2s-objs := atmel-i2s.o +snd-soc-mchp-i2s-mcc-objs := mchp-i2s-mcc.o
obj-$(CONFIG_SND_ATMEL_SOC_PDC) += snd-soc-atmel-pcm-pdc.o obj-$(CONFIG_SND_ATMEL_SOC_DMA) += snd-soc-atmel-pcm-dma.o obj-$(CONFIG_SND_ATMEL_SOC_SSC) += snd-soc-atmel_ssc_dai.o obj-$(CONFIG_SND_ATMEL_SOC_I2S) += snd-soc-atmel-i2s.o +obj-$(CONFIG_SND_MCHP_SOC_I2S_MCC) += snd-soc-mchp-i2s-mcc.o
# AT91 Machine Support snd-soc-sam9g20-wm8731-objs := sam9g20_wm8731.o diff --git a/sound/soc/atmel/mchp-i2s-mcc.c b/sound/soc/atmel/mchp-i2s-mcc.c new file mode 100644 index 000000000000..86495883ca3f --- /dev/null +++ b/sound/soc/atmel/mchp-i2s-mcc.c @@ -0,0 +1,974 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Driver for Microchip I2S Multi-channel controller +// +// Copyright (C) 2018 Microchip Technology Inc. and its subsidiaries +// +// Author: Codrin Ciubotariu codrin.ciubotariu@microchip.com + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/slab.h> + +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/clk.h> +#include <linux/mfd/syscon.h> +#include <linux/lcm.h> + +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/initval.h> +#include <sound/soc.h> +#include <sound/dmaengine_pcm.h> + +/* + * ---- I2S Controller Register map ---- + */ +#define MCHP_I2SMCC_CR 0x0000 /* Control Register */ +#define MCHP_I2SMCC_MRA 0x0004 /* Mode Register A */ +#define MCHP_I2SMCC_MRB 0x0008 /* Mode Register B */ +#define MCHP_I2SMCC_SR 0x000C /* Status Register */ +#define MCHP_I2SMCC_IERA 0x0010 /* Interrupt Enable Register A */ +#define MCHP_I2SMCC_IDRA 0x0014 /* Interrupt Disable Register A */ +#define MCHP_I2SMCC_IMRA 0x0018 /* Interrupt Mask Register A */ +#define MCHP_I2SMCC_ISRA 0X001C /* Interrupt Status Register A */ + +#define MCHP_I2SMCC_IERB 0x0020 /* Interrupt Enable Register B */ +#define MCHP_I2SMCC_IDRB 0x0024 /* Interrupt Disable Register B */ +#define MCHP_I2SMCC_IMRB 0x0028 /* Interrupt Mask Register B */ +#define MCHP_I2SMCC_ISRB 0X002C /* Interrupt Status Register B */ + +#define MCHP_I2SMCC_RHR 0x0030 /* Receiver Holding Register */ +#define MCHP_I2SMCC_THR 0x0034 /* Transmitter Holding Register */ + +#define MCHP_I2SMCC_RHL0R 0x0040 /* Receiver Holding Left 0 Register */ +#define MCHP_I2SMCC_RHR0R 0x0044 /* Receiver Holding Right 0 Register */ + +#define MCHP_I2SMCC_RHL1R 0x0048 /* Receiver Holding Left 1 Register */ +#define MCHP_I2SMCC_RHR1R 0x004C /* Receiver Holding Right 1 Register */ + +#define MCHP_I2SMCC_RHL2R 0x0050 /* Receiver Holding Left 2 Register */ +#define MCHP_I2SMCC_RHR2R 0x0054 /* Receiver Holding Right 2 Register */ + +#define MCHP_I2SMCC_RHL3R 0x0058 /* Receiver Holding Left 3 Register */ +#define MCHP_I2SMCC_RHR3R 0x005C /* Receiver Holding Right 3 Register */ + +#define MCHP_I2SMCC_THL0R 0x0060 /* Transmitter Holding Left 0 Register */ +#define MCHP_I2SMCC_THR0R 0x0064 /* Transmitter Holding Right 0 Register */ + +#define MCHP_I2SMCC_THL1R 0x0068 /* Transmitter Holding Left 1 Register */ +#define MCHP_I2SMCC_THR1R 0x006C /* Transmitter Holding Right 1 Register */ + +#define MCHP_I2SMCC_THL2R 0x0070 /* Transmitter Holding Left 2 Register */ +#define MCHP_I2SMCC_THR2R 0x0074 /* Transmitter Holding Right 2 Register */ + +#define MCHP_I2SMCC_THL3R 0x0078 /* Transmitter Holding Left 3 Register */ +#define MCHP_I2SMCC_THR3R 0x007C /* Transmitter Holding Right 3 Register */ + +#define MCHP_I2SMCC_VERSION 0x00FC /* Version Register */ + +/* + * ---- Control Register (Write-only) ---- + */ +#define MCHP_I2SMCC_CR_RXEN BIT(0) /* Receiver Enable */ +#define MCHP_I2SMCC_CR_RXDIS BIT(1) /* Receiver Disable */ +#define MCHP_I2SMCC_CR_CKEN BIT(2) /* Clock Enable */ +#define MCHP_I2SMCC_CR_CKDIS BIT(3) /* Clock Disable */ +#define MCHP_I2SMCC_CR_TXEN BIT(4) /* Transmitter Enable */ +#define MCHP_I2SMCC_CR_TXDIS BIT(5) /* Transmitter Disable */ +#define MCHP_I2SMCC_CR_SWRST BIT(7) /* Software Reset */ + +/* + * ---- Mode Register A (Read/Write) ---- + */ +#define MCHP_I2SMCC_MRA_MODE_MASK GENMASK(0, 0) +#define MCHP_I2SMCC_MRA_MODE_SLAVE (0 << 0) +#define MCHP_I2SMCC_MRA_MODE_MASTER (1 << 0) + +#define MCHP_I2SMCC_MRA_DATALENGTH_MASK GENMASK(3, 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_32_BITS (0 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_24_BITS (1 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_20_BITS (2 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_18_BITS (3 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_16_BITS (4 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_16_BITS_COMPACT (5 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_8_BITS (6 << 1) +#define MCHP_I2SMCC_MRA_DATALENGTH_8_BITS_COMPACT (7 << 1) + +#define MCHP_I2SMCC_MRA_WIRECFG_MASK GENMASK(5, 4) +#define MCHP_I2SMCC_MRA_WIRECFG_I2S_1_TDM_0 (0 << 4) +#define MCHP_I2SMCC_MRA_WIRECFG_I2S_2_TDM_1 (1 << 4) +#define MCHP_I2SMCC_MRA_WIRECFG_I2S_4_TDM_2 (2 << 4) +#define MCHP_I2SMCC_MRA_WIRECFG_TDM_3 (3 << 4) + +#define MCHP_I2SMCC_MRA_FORMAT_MASK GENMASK(7, 6) +#define MCHP_I2SMCC_MRA_FORMAT_I2S (0 << 6) +#define MCHP_I2SMCC_MRA_FORMAT_LJ (1 << 6) /* Left Justified */ +#define MCHP_I2SMCC_MRA_FORMAT_TDM (2 << 6) +#define MCHP_I2SMCC_MRA_FORMAT_TDMLJ (3 << 6) + +/* Transmitter uses one DMA channel ... */ +/* Left audio samples duplicated to right audio channel */ +#define MCHP_I2SMCC_MRA_RXMONO BIT(8) + +/* I2SDO output of I2SC is internally connected to I2SDI input */ +#define MCHP_I2SMCC_MRA_RXLOOP BIT(9) + +/* Receiver uses one DMA channel ... */ +/* Left audio samples duplicated to right audio channel */ +#define MCHP_I2SMCC_MRA_TXMONO BIT(10) + +/* x sample transmitted when underrun */ +#define MCHP_I2SMCC_MRA_TXSAME_ZERO (0 << 11) /* Zero sample */ +#define MCHP_I2SMCC_MRA_TXSAME_PREVIOUS (1 << 11) /* Previous sample */ + +/* select between peripheral clock and generated clock */ +#define MCHP_I2SMCC_MRA_SRCCLK_PCLK (0 << 12) +#define MCHP_I2SMCC_MRA_SRCCLK_GCLK (1 << 12) + +/* Number of TDM Channels - 1 */ +#define MCHP_I2SMCC_MRA_NBCHAN_MASK GENMASK(15, 13) +#define MCHP_I2SMCC_MRA_NBCHAN(ch) \ + ((((ch) - 1) << 13) & MCHP_I2SMCC_MRA_NBCHAN_MASK) + +/* Selected Clock to I2SMCC Master Clock ratio */ +#define MCHP_I2SMCC_MRA_IMCKDIV_MASK GENMASK(21, 16) +#define MCHP_I2SMCC_MRA_IMCKDIV(div) \ + (((div) << 16) & MCHP_I2SMCC_MRA_IMCKDIV_MASK) + +/* TDM Frame Synchronization */ +#define MCHP_I2SMCC_MRA_TDMFS_MASK GENMASK(23, 22) +#define MCHP_I2SMCC_MRA_TDMFS_SLOT (0 << 22) +#define MCHP_I2SMCC_MRA_TDMFS_HALF (1 << 22) +#define MCHP_I2SMCC_MRA_TDMFS_BIT (2 << 22) + +/* Selected Clock to I2SMC Serial Clock ratio */ +#define MCHP_I2SMCC_MRA_ISCKDIV_MASK GENMASK(29, 24) +#define MCHP_I2SMCC_MRA_ISCKDIV(div) \ + (((div) << 24) & MCHP_I2SMCC_MRA_ISCKDIV_MASK) + +/* Master Clock mode */ +#define MCHP_I2SMCC_MRA_IMCKMODE_MASK GENMASK(30, 30) +/* 0: No master clock generated*/ +#define MCHP_I2SMCC_MRA_IMCKMODE_NONE (0 << 30) +/* 1: master clock generated (internally generated clock drives I2SMCK pin) */ +#define MCHP_I2SMCC_MRA_IMCKMODE_GEN (1 << 30) + +/* Slot Width */ +/* 0: slot is 32 bits wide for DATALENGTH = 18/20/24 bits. */ +/* 1: slot is 24 bits wide for DATALENGTH = 18/20/24 bits. */ +#define MCHP_I2SMCC_MRA_IWS BIT(31) + +/* + * ---- Mode Register B (Read/Write) ---- + */ +/* all enabled I2S left channels are filled first, then I2S right channels */ +#define MCHP_I2SMCC_MRB_CRAMODE_LEFT_FIRST (0 << 0) +/* + * an enabled I2S left channel is filled, then the corresponding right + * channel, until all channels are filled + */ +#define MCHP_I2SMCC_MRB_CRAMODE_REGULAR (1 << 0) + +#define MCHP_I2SMCC_MRB_FIFOEN BIT(1) + +#define MCHP_I2SMCC_MRB_DMACHUNK_MASK GENMASK(9, 8) +#define MCHP_I2SMCC_MRB_DMACHUNK(no_words) \ + (((fls(no_words) - 1) << 8) & MCHP_I2SMCC_MRB_DMACHUNK_MASK) + +#define MCHP_I2SMCC_MRB_CLKSEL_MASK GENMASK(16, 16) +#define MCHP_I2SMCC_MRB_CLKSEL_EXT (0 << 16) +#define MCHP_I2SMCC_MRB_CLKSEL_INT (1 << 16) + +/* + * ---- Status Registers (Read-only) ---- + */ +#define MCHP_I2SMCC_SR_RXEN BIT(0) /* Receiver Enabled */ +#define MCHP_I2SMCC_SR_TXEN BIT(4) /* Transmitter Enabled */ + +/* + * ---- Interrupt Enable/Disable/Mask/Status Registers A ---- + */ +#define MCHP_I2SMCC_INT_TXRDY_MASK(ch) GENMASK((ch) - 1, 0) +#define MCHP_I2SMCC_INT_TXRDYCH(ch) BIT(ch) +#define MCHP_I2SMCC_INT_TXUNF_MASK(ch) GENMASK((ch) + 7, 8) +#define MCHP_I2SMCC_INT_TXUNFCH(ch) BIT((ch) + 8) +#define MCHP_I2SMCC_INT_RXRDY_MASK(ch) GENMASK((ch) + 15, 16) +#define MCHP_I2SMCC_INT_RXRDYCH(ch) BIT((ch) + 16) +#define MCHP_I2SMCC_INT_RXOVF_MASK(ch) GENMASK((ch) + 23, 24) +#define MCHP_I2SMCC_INT_RXOVFCH(ch) BIT((ch) + 24) + +/* + * ---- Interrupt Enable/Disable/Mask/Status Registers B ---- + */ +#define MCHP_I2SMCC_INT_WERR BIT(0) +#define MCHP_I2SMCC_INT_TXFFRDY BIT(8) +#define MCHP_I2SMCC_INT_TXFFEMP BIT(9) +#define MCHP_I2SMCC_INT_RXFFRDY BIT(12) +#define MCHP_I2SMCC_INT_RXFFFUL BIT(13) + +/* + * ---- Version Register (Read-only) ---- + */ +#define MCHP_I2SMCC_VERSION_MASK GENMASK(11, 0) + +#define MCHP_I2SMCC_MAX_CHANNELS 8 +#define MCHP_I2MCC_TDM_SLOT_WIDTH 32 + +static const struct regmap_config mchp_i2s_mcc_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .max_register = MCHP_I2SMCC_VERSION, +}; + +struct mchp_i2s_mcc_dev { + struct wait_queue_head wq_txrdy; + struct wait_queue_head wq_rxrdy; + struct device *dev; + struct regmap *regmap; + struct clk *pclk; + struct clk *gclk; + struct snd_dmaengine_dai_dma_data playback; + struct snd_dmaengine_dai_dma_data capture; + unsigned int fmt; + unsigned int sysclk; + unsigned int frame_length; + int tdm_slots; + int channels; + int gclk_use:1; + int gclk_running:1; + int tx_rdy:1; + int rx_rdy:1; +}; + +static irqreturn_t mchp_i2s_mcc_interrupt(int irq, void *dev_id) +{ + struct mchp_i2s_mcc_dev *dev = dev_id; + u32 sra, imra, srb, imrb, pendinga, pendingb, idra = 0; + irqreturn_t ret = IRQ_NONE; + + regmap_read(dev->regmap, MCHP_I2SMCC_IMRA, &imra); + regmap_read(dev->regmap, MCHP_I2SMCC_ISRA, &sra); + pendinga = imra & sra; + + regmap_read(dev->regmap, MCHP_I2SMCC_IMRB, &imrb); + regmap_read(dev->regmap, MCHP_I2SMCC_ISRB, &srb); + pendingb = imrb & srb; + + if (!pendinga && !pendingb) + return IRQ_NONE; + + /* + * Tx/Rx ready interrupts are enabled when stopping only, to assure + * availability and to disable clocks if necessary + */ + idra |= pendinga & (MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels) | + MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels)); + if (idra) + ret = IRQ_HANDLED; + + if ((imra & MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels)) && + (imra & MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels)) == + (idra & MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels))) { + dev->tx_rdy = 1; + wake_up_interruptible(&dev->wq_txrdy); + } + if ((imra & MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels)) && + (imra & MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels)) == + (idra & MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels))) { + dev->rx_rdy = 1; + wake_up_interruptible(&dev->wq_rxrdy); + } + regmap_write(dev->regmap, MCHP_I2SMCC_IDRA, idra); + + return ret; +} + +static int mchp_i2s_mcc_set_sysclk(struct snd_soc_dai *dai, + int clk_id, unsigned int freq, int dir) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + dev_dbg(dev->dev, "%s() clk_id=%d freq=%u dir=%d\n", + __func__, clk_id, freq, dir); + + /* We do not need SYSCLK */ + if (dir == SND_SOC_CLOCK_IN) + return 0; + + dev->sysclk = freq; + + return 0; +} + +static int mchp_i2s_mcc_set_bclk_ratio(struct snd_soc_dai *dai, + unsigned int ratio) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + dev_dbg(dev->dev, "%s() ratio=%u\n", __func__, ratio); + + dev->frame_length = ratio; + + return 0; +} + +static int mchp_i2s_mcc_set_dai_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + dev_dbg(dev->dev, "%s() fmt=%#x\n", __func__, fmt); + + /* We don't support any kind of clock inversion */ + if ((fmt & SND_SOC_DAIFMT_INV_MASK) != SND_SOC_DAIFMT_NB_NF) + return -EINVAL; + + /* We can't generate only FSYNC */ + if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) == SND_SOC_DAIFMT_CBM_CFS) + return -EINVAL; + + /* We can only reconfigure the IP when it's stopped */ + if (fmt & SND_SOC_DAIFMT_CONT) + return -EINVAL; + + dev->fmt = fmt; + + return 0; +} + +static int mchp_i2s_mcc_set_dai_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, + unsigned int rx_mask, + int slots, int slot_width) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + dev_dbg(dev->dev, + "%s() tx_mask=0x%08x rx_mask=0x%08x slots=%d width=%d\n", + __func__, tx_mask, rx_mask, slots, slot_width); + + if (slots < 0 || slots > MCHP_I2SMCC_MAX_CHANNELS || + slot_width != MCHP_I2MCC_TDM_SLOT_WIDTH) + return -EINVAL; + + if (slots) { + /* We do not support daisy chain */ + if (rx_mask != GENMASK(slots - 1, 0) || + rx_mask != tx_mask) + return -EINVAL; + } + + dev->tdm_slots = slots; + dev->frame_length = slots * MCHP_I2MCC_TDM_SLOT_WIDTH; + + return 0; +} + +static int mchp_i2s_mcc_clk_get_rate_diff(struct clk *clk, + unsigned long rate, + struct clk **best_clk, + unsigned long *best_rate, + unsigned long *best_diff_rate) +{ + long round_rate; + unsigned int diff_rate; + + round_rate = clk_round_rate(clk, rate); + if (round_rate < 0) + return (int)round_rate; + + diff_rate = abs(rate - round_rate); + if (diff_rate < *best_diff_rate) { + *best_clk = clk; + *best_diff_rate = diff_rate; + *best_rate = rate; + } + + return 0; +} + +static int mchp_i2s_mcc_config_divs(struct mchp_i2s_mcc_dev *dev, + unsigned int bclk, unsigned int *mra) +{ + unsigned long clk_rate; + unsigned long lcm_rate; + unsigned long best_rate = 0; + unsigned long best_diff_rate = ~0; + unsigned int sysclk; + struct clk *best_clk = NULL; + int ret; + + /* For code simplification */ + if (!dev->sysclk) + sysclk = bclk; + else + sysclk = dev->sysclk; + + /* + * MCLK is Selected CLK / (2 * IMCKDIV), + * BCLK is Selected CLK / (2 * ISCKDIV); + * if IMCKDIV or ISCKDIV are 0, MCLK or BCLK = Selected CLK + */ + lcm_rate = lcm(sysclk, bclk); + if ((lcm_rate / sysclk % 2 == 1 && lcm_rate / sysclk > 2) || + (lcm_rate / bclk % 2 == 1 && lcm_rate / bclk > 2)) + lcm_rate *= 2; + + for (clk_rate = lcm_rate; + (clk_rate == sysclk || clk_rate / (sysclk * 2) <= GENMASK(5, 0)) && + (clk_rate == bclk || clk_rate / (bclk * 2) <= GENMASK(5, 0)); + clk_rate += lcm_rate) { + ret = mchp_i2s_mcc_clk_get_rate_diff(dev->gclk, clk_rate, + &best_clk, &best_rate, + &best_diff_rate); + if (ret) { + dev_err(dev->dev, "gclk error for rate %lu: %d", + clk_rate, ret); + } else { + if (!best_diff_rate) { + dev_dbg(dev->dev, "found perfect rate on gclk: %lu\n", + clk_rate); + break; + } + } + + ret = mchp_i2s_mcc_clk_get_rate_diff(dev->pclk, clk_rate, + &best_clk, &best_rate, + &best_diff_rate); + if (ret) { + dev_err(dev->dev, "pclk error for rate %lu: %d", + clk_rate, ret); + } else { + if (!best_diff_rate) { + dev_dbg(dev->dev, "found perfect rate on pclk: %lu\n", + clk_rate); + break; + } + } + } + + /* check if clocks returned only errors */ + if (!best_clk) { + dev_err(dev->dev, "unable to change rate to clocks\n"); + return -EINVAL; + } + + dev_dbg(dev->dev, "source CLK is %s with rate %lu, diff %lu\n", + best_clk == dev->pclk ? "pclk" : "gclk", + best_rate, best_diff_rate); + + /* set the rate */ + ret = clk_set_rate(best_clk, best_rate); + if (ret) { + dev_err(dev->dev, "unable to set rate %lu to %s: %d\n", + best_rate, best_clk == dev->pclk ? "PCLK" : "GCLK", + ret); + return ret; + } + + /* Configure divisors */ + if (dev->sysclk) + *mra |= MCHP_I2SMCC_MRA_IMCKDIV(best_rate / (2 * sysclk)); + *mra |= MCHP_I2SMCC_MRA_ISCKDIV(best_rate / (2 * bclk)); + + if (best_clk == dev->gclk) { + *mra |= MCHP_I2SMCC_MRA_SRCCLK_GCLK; + ret = clk_prepare(dev->gclk); + if (ret < 0) + dev_err(dev->dev, "unable to prepare GCLK: %d\n", ret); + else + dev->gclk_use = 1; + } else { + *mra |= MCHP_I2SMCC_MRA_SRCCLK_PCLK; + dev->gclk_use = 0; + } + + return 0; +} + +static int mchp_i2s_mcc_is_running(struct mchp_i2s_mcc_dev *dev) +{ + u32 sr; + + regmap_read(dev->regmap, MCHP_I2SMCC_SR, &sr); + return !!(sr & (MCHP_I2SMCC_SR_TXEN | MCHP_I2SMCC_SR_RXEN)); +} + +static int mchp_i2s_mcc_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + u32 mra = 0; + u32 mrb = 0; + unsigned int channels = params_channels(params); + unsigned int frame_length = dev->frame_length; + unsigned int bclk_rate; + int set_divs = 0; + int ret; + bool is_playback = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK); + + dev_dbg(dev->dev, "%s() rate=%u format=%#x width=%u channels=%u\n", + __func__, params_rate(params), params_format(params), + params_width(params), params_channels(params)); + + switch (dev->fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + if (dev->tdm_slots) { + dev_err(dev->dev, "I2S with TDM is not supported\n"); + return -EINVAL; + } + mra |= MCHP_I2SMCC_MRA_FORMAT_I2S; + break; + case SND_SOC_DAIFMT_LEFT_J: + if (dev->tdm_slots) { + dev_err(dev->dev, "Left-Justified with TDM is not supported\n"); + return -EINVAL; + } + mra |= MCHP_I2SMCC_MRA_FORMAT_LJ; + break; + case SND_SOC_DAIFMT_DSP_A: + mra |= MCHP_I2SMCC_MRA_FORMAT_TDM; + break; + default: + dev_err(dev->dev, "unsupported bus format\n"); + return -EINVAL; + } + + switch (dev->fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBS_CFS: + /* cpu is BCLK and LRC master */ + mra |= MCHP_I2SMCC_MRA_MODE_MASTER; + if (dev->sysclk) + mra |= MCHP_I2SMCC_MRA_IMCKMODE_GEN; + set_divs = 1; + break; + case SND_SOC_DAIFMT_CBS_CFM: + /* cpu is BCLK master */ + mrb |= MCHP_I2SMCC_MRB_CLKSEL_INT; + set_divs = 1; + /* fall through */ + case SND_SOC_DAIFMT_CBM_CFM: + /* cpu is slave */ + mra |= MCHP_I2SMCC_MRA_MODE_SLAVE; + if (dev->sysclk) + dev_warn(dev->dev, "Unable to generate MCLK in Slave mode\n"); + break; + default: + dev_err(dev->dev, "unsupported master/slave mode\n"); + return -EINVAL; + } + + if (dev->fmt & (SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_LEFT_J)) { + switch (channels) { + case 1: + if (is_playback) + mra |= MCHP_I2SMCC_MRA_TXMONO; + else + mra |= MCHP_I2SMCC_MRA_RXMONO; + break; + case 2: + break; + default: + dev_err(dev->dev, "unsupported number of audio channels\n"); + return -EINVAL; + } + + if (!frame_length) + frame_length = 2 * params_physical_width(params); + } else if (dev->fmt & SND_SOC_DAIFMT_DSP_A) { + if (dev->tdm_slots) { + if (channels % 2 && channels * 2 <= dev->tdm_slots) { + /* + * Duplicate data for even-numbered channels + * to odd-numbered channels + */ + if (is_playback) + mra |= MCHP_I2SMCC_MRA_TXMONO; + else + mra |= MCHP_I2SMCC_MRA_RXMONO; + } + channels = dev->tdm_slots; + } + + mra |= MCHP_I2SMCC_MRA_NBCHAN(channels); + if (!frame_length) + frame_length = channels * MCHP_I2MCC_TDM_SLOT_WIDTH; + } + + /* + * We must have the same burst size configured + * in the DMA transfer and in out IP + */ + mrb |= MCHP_I2SMCC_MRB_DMACHUNK(channels); + if (is_playback) + dev->playback.maxburst = 1 << (fls(channels) - 1); + else + dev->capture.maxburst = 1 << (fls(channels) - 1); + + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S8: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_8_BITS; + break; + case SNDRV_PCM_FORMAT_S16_LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_16_BITS; + break; + case SNDRV_PCM_FORMAT_S18_3LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_18_BITS | + MCHP_I2SMCC_MRA_IWS; + break; + case SNDRV_PCM_FORMAT_S20_3LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_20_BITS | + MCHP_I2SMCC_MRA_IWS; + break; + case SNDRV_PCM_FORMAT_S24_3LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_24_BITS | + MCHP_I2SMCC_MRA_IWS; + break; + case SNDRV_PCM_FORMAT_S24_LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_24_BITS; + break; + case SNDRV_PCM_FORMAT_S32_LE: + mra |= MCHP_I2SMCC_MRA_DATALENGTH_32_BITS; + break; + default: + dev_err(dev->dev, "unsupported size/endianness for audio samples\n"); + return -EINVAL; + } + + /* + * If we are already running, the wanted setup must be + * the same with the one that's currently ongoing + */ + if (mchp_i2s_mcc_is_running(dev)) { + u32 mra_cur; + u32 mrb_cur; + + regmap_read(dev->regmap, MCHP_I2SMCC_MRA, &mra_cur); + regmap_read(dev->regmap, MCHP_I2SMCC_MRB, &mrb_cur); + if (mra != mra_cur || mrb != mrb_cur) + return -EINVAL; + + return 0; + } + + /* Save the number of channels to know what interrupts to enable */ + dev->channels = channels; + + if (set_divs) { + bclk_rate = frame_length * params_rate(params); + ret = mchp_i2s_mcc_config_divs(dev, bclk_rate, &mra); + if (ret) { + dev_err(dev->dev, "unable to configure the divisors: %d\n", + ret); + return ret; + } + } + + ret = regmap_write(dev->regmap, MCHP_I2SMCC_MRA, mra); + if (ret < 0) + return ret; + return regmap_write(dev->regmap, MCHP_I2SMCC_MRB, mrb); +} + +static int mchp_i2s_mcc_hw_free(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + bool is_playback = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK); + long err; + + if (is_playback) { + err = wait_event_interruptible_timeout(dev->wq_txrdy, + dev->tx_rdy, + msecs_to_jiffies(500)); + } else { + err = wait_event_interruptible_timeout(dev->wq_rxrdy, + dev->rx_rdy, + msecs_to_jiffies(500)); + } + + if (err == 0) { + u32 idra; + + dev_warn_once(dev->dev, "Timeout waiting for %s\n", + is_playback ? "Tx ready" : "Rx ready"); + if (is_playback) + idra = MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels); + else + idra = MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels); + regmap_write(dev->regmap, MCHP_I2SMCC_IDRA, idra); + } + + if (!mchp_i2s_mcc_is_running(dev)) { + regmap_write(dev->regmap, MCHP_I2SMCC_CR, MCHP_I2SMCC_CR_CKDIS); + + if (dev->gclk_running) { + clk_disable_unprepare(dev->gclk); + dev->gclk_running = 0; + } + } + + return 0; +} + +static int mchp_i2s_mcc_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + bool is_playback = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK); + u32 cr = 0; + u32 iera = 0; + u32 sr; + int err; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + if (is_playback) + cr = MCHP_I2SMCC_CR_TXEN | MCHP_I2SMCC_CR_CKEN; + else + cr = MCHP_I2SMCC_CR_RXEN | MCHP_I2SMCC_CR_CKEN; + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + regmap_read(dev->regmap, MCHP_I2SMCC_SR, &sr); + if (is_playback && (sr & MCHP_I2SMCC_SR_TXEN)) { + cr = MCHP_I2SMCC_CR_TXDIS; + dev->tx_rdy = 0; + /* + * Enable Tx Ready interrupts on all channels + * to assure all data is sent + */ + iera = MCHP_I2SMCC_INT_TXRDY_MASK(dev->channels); + } else if (!is_playback && (sr & MCHP_I2SMCC_SR_RXEN)) { + cr = MCHP_I2SMCC_CR_RXDIS; + dev->rx_rdy = 0; + /* + * Enable Rx Ready interrupts on all channels + * to assure all data is received + */ + iera = MCHP_I2SMCC_INT_RXRDY_MASK(dev->channels); + } + break; + default: + return -EINVAL; + } + + if ((cr & MCHP_I2SMCC_CR_CKEN) && dev->gclk_use && + !dev->gclk_running) { + err = clk_enable(dev->gclk); + if (err) { + dev_err_once(dev->dev, "failed to enable GCLK: %d\n", + err); + } else { + dev->gclk_running = 1; + } + } + + regmap_write(dev->regmap, MCHP_I2SMCC_IERA, iera); + regmap_write(dev->regmap, MCHP_I2SMCC_CR, cr); + + return 0; +} + +static int mchp_i2s_mcc_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + /* Software reset the IP if it's not running */ + if (!mchp_i2s_mcc_is_running(dev)) { + return regmap_write(dev->regmap, MCHP_I2SMCC_CR, + MCHP_I2SMCC_CR_SWRST); + } + + return 0; +} + +static const struct snd_soc_dai_ops mchp_i2s_mcc_dai_ops = { + .set_sysclk = mchp_i2s_mcc_set_sysclk, + .set_bclk_ratio = mchp_i2s_mcc_set_bclk_ratio, + .startup = mchp_i2s_mcc_startup, + .trigger = mchp_i2s_mcc_trigger, + .hw_params = mchp_i2s_mcc_hw_params, + .hw_free = mchp_i2s_mcc_hw_free, + .set_fmt = mchp_i2s_mcc_set_dai_fmt, + .set_tdm_slot = mchp_i2s_mcc_set_dai_tdm_slot, +}; + +static int mchp_i2s_mcc_dai_probe(struct snd_soc_dai *dai) +{ + struct mchp_i2s_mcc_dev *dev = snd_soc_dai_get_drvdata(dai); + + init_waitqueue_head(&dev->wq_txrdy); + init_waitqueue_head(&dev->wq_rxrdy); + + snd_soc_dai_init_dma_data(dai, &dev->playback, &dev->capture); + + return 0; +} + +#define MCHP_I2SMCC_RATES SNDRV_PCM_RATE_8000_192000 + +#define MCHP_I2SMCC_FORMATS (SNDRV_PCM_FMTBIT_S8 | \ + SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S18_3LE | \ + SNDRV_PCM_FMTBIT_S20_3LE | \ + SNDRV_PCM_FMTBIT_S24_3LE | \ + SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE) + +static struct snd_soc_dai_driver mchp_i2s_mcc_dai = { + .probe = mchp_i2s_mcc_dai_probe, + .playback = { + .stream_name = "I2SMCC-Playback", + .channels_min = 1, + .channels_max = 8, + .rates = MCHP_I2SMCC_RATES, + .formats = MCHP_I2SMCC_FORMATS, + }, + .capture = { + .stream_name = "I2SMCC-Capture", + .channels_min = 1, + .channels_max = 8, + .rates = MCHP_I2SMCC_RATES, + .formats = MCHP_I2SMCC_FORMATS, + }, + .ops = &mchp_i2s_mcc_dai_ops, + .symmetric_rates = 1, + .symmetric_samplebits = 1, + .symmetric_channels = 1, +}; + +static const struct snd_soc_component_driver mchp_i2s_mcc_component = { + .name = "mchp-i2s-mcc", +}; + +#ifdef CONFIG_OF +static const struct of_device_id mchp_i2s_mcc_dt_ids[] = { + { + .compatible = "microchip,sam9x60-i2smcc", + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, mchp_i2s_mcc_dt_ids); +#endif + +static int mchp_i2s_mcc_probe(struct platform_device *pdev) +{ + struct mchp_i2s_mcc_dev *dev; + struct resource *mem; + struct regmap *regmap; + void __iomem *base; + u32 version; + int irq; + int err; + + dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); + base = devm_ioremap_resource(&pdev->dev, mem); + if (IS_ERR(base)) + return PTR_ERR(base); + + regmap = devm_regmap_init_mmio(&pdev->dev, base, + &mchp_i2s_mcc_regmap_config); + if (IS_ERR(regmap)) + return PTR_ERR(regmap); + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + err = devm_request_irq(&pdev->dev, irq, mchp_i2s_mcc_interrupt, 0, + dev_name(&pdev->dev), dev); + if (err) + return err; + + dev->pclk = devm_clk_get(&pdev->dev, "pclk"); + if (IS_ERR(dev->pclk)) { + err = PTR_ERR(dev->pclk); + dev_err(&pdev->dev, + "failed to get the peripheral clock: %d\n", err); + return err; + } + + /* Get the optional generated clock */ + dev->gclk = devm_clk_get(&pdev->dev, "gclk"); + if (IS_ERR(dev->gclk)) { + if (PTR_ERR(dev->gclk) == -EPROBE_DEFER) + return -EPROBE_DEFER; + dev_warn(&pdev->dev, + "generated clock not found: %d\n", err); + dev->gclk = NULL; + } + + dev->dev = &pdev->dev; + dev->regmap = regmap; + platform_set_drvdata(pdev, dev); + + err = clk_prepare_enable(dev->pclk); + if (err) { + dev_err(&pdev->dev, + "failed to enable the peripheral clock: %d\n", err); + return err; + } + + err = devm_snd_soc_register_component(&pdev->dev, + &mchp_i2s_mcc_component, + &mchp_i2s_mcc_dai, 1); + if (err) { + dev_err(&pdev->dev, "failed to register DAI: %d\n", err); + clk_disable_unprepare(dev->pclk); + return err; + } + + dev->playback.addr = (dma_addr_t)mem->start + MCHP_I2SMCC_THR; + dev->capture.addr = (dma_addr_t)mem->start + MCHP_I2SMCC_RHR; + + err = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0); + if (err) { + dev_err(&pdev->dev, "failed to register PCM: %d\n", err); + clk_disable_unprepare(dev->pclk); + return err; + } + + /* Get IP version. */ + regmap_read(dev->regmap, MCHP_I2SMCC_VERSION, &version); + dev_info(&pdev->dev, "hw version: %#lx\n", + version & MCHP_I2SMCC_VERSION_MASK); + + return 0; +} + +static int mchp_i2s_mcc_remove(struct platform_device *pdev) +{ + struct mchp_i2s_mcc_dev *dev = platform_get_drvdata(pdev); + + clk_disable_unprepare(dev->pclk); + + return 0; +} + +static struct platform_driver mchp_i2s_mcc_driver = { + .driver = { + .name = "mchp_i2s_mcc", + .of_match_table = of_match_ptr(mchp_i2s_mcc_dt_ids), + }, + .probe = mchp_i2s_mcc_probe, + .remove = mchp_i2s_mcc_remove, +}; +module_platform_driver(mchp_i2s_mcc_driver); + +MODULE_DESCRIPTION("Microchip I2S Multi-Channel Controller driver"); +MODULE_AUTHOR("Codrin Ciubotariu codrin.ciubotariu@microchip.com"); +MODULE_LICENSE("GPL v2");