diff options
Diffstat (limited to 'sound/soc/bcm')
-rw-r--r-- | sound/soc/bcm/Kconfig | 28 | ||||
-rw-r--r-- | sound/soc/bcm/Makefile | 15 | ||||
-rw-r--r-- | sound/soc/bcm/bcm2835-i2s.c | 935 | ||||
-rw-r--r-- | sound/soc/bcm/bcm63xx-i2s-whistler.c | 317 | ||||
-rw-r--r-- | sound/soc/bcm/bcm63xx-i2s.h | 90 | ||||
-rw-r--r-- | sound/soc/bcm/bcm63xx-pcm-whistler.c | 485 | ||||
-rw-r--r-- | sound/soc/bcm/cygnus-pcm.c | 861 | ||||
-rw-r--r-- | sound/soc/bcm/cygnus-ssp.c | 1417 | ||||
-rw-r--r-- | sound/soc/bcm/cygnus-ssp.h | 139 |
9 files changed, 4287 insertions, 0 deletions
diff --git a/sound/soc/bcm/Kconfig b/sound/soc/bcm/Kconfig new file mode 100644 index 000000000..4218057b0 --- /dev/null +++ b/sound/soc/bcm/Kconfig @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-only +config SND_BCM2835_SOC_I2S + tristate "SoC Audio support for the Broadcom BCM2835 I2S module" + depends on ARCH_BCM2835 || COMPILE_TEST + select SND_SOC_GENERIC_DMAENGINE_PCM + select REGMAP_MMIO + help + Say Y or M if you want to add support for codecs attached to + the BCM2835 I2S interface. You will also need + to select the audio interfaces to support below. + +config SND_SOC_CYGNUS + tristate "SoC platform audio for Broadcom Cygnus chips" + depends on ARCH_BCM_CYGNUS || COMPILE_TEST + help + Say Y if you want to add support for ASoC audio on Broadcom + Cygnus chips (bcm958300, bcm958305, bcm911360) + + If you don't know what to do here, say N. + +config SND_BCM63XX_I2S_WHISTLER + tristate "SoC Audio support for the Broadcom BCM63XX I2S module" + select REGMAP_MMIO + help + Say Y if you want to add support for ASoC audio on Broadcom + DSL/PON chips (bcm63158, bcm63178) + + If you don't know what to do here, say N diff --git a/sound/soc/bcm/Makefile b/sound/soc/bcm/Makefile new file mode 100644 index 000000000..7c2d78996 --- /dev/null +++ b/sound/soc/bcm/Makefile @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only +# BCM2835 Platform Support +snd-soc-bcm2835-i2s-objs := bcm2835-i2s.o + +obj-$(CONFIG_SND_BCM2835_SOC_I2S) += snd-soc-bcm2835-i2s.o + +# CYGNUS Platform Support +snd-soc-cygnus-objs := cygnus-pcm.o cygnus-ssp.o + +obj-$(CONFIG_SND_SOC_CYGNUS) += snd-soc-cygnus.o + +# BCM63XX Platform Support +snd-soc-63xx-objs := bcm63xx-i2s-whistler.o bcm63xx-pcm-whistler.o + +obj-$(CONFIG_SND_BCM63XX_I2S_WHISTLER) += snd-soc-63xx.o
\ No newline at end of file diff --git a/sound/soc/bcm/bcm2835-i2s.c b/sound/soc/bcm/bcm2835-i2s.c new file mode 100644 index 000000000..dc34fe155 --- /dev/null +++ b/sound/soc/bcm/bcm2835-i2s.c @@ -0,0 +1,935 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ALSA SoC I2S Audio Layer for Broadcom BCM2835 SoC + * + * Author: Florian Meier <florian.meier@koalo.de> + * Copyright 2013 + * + * Based on + * Raspberry Pi PCM I2S ALSA Driver + * Copyright (c) by Phil Poole 2013 + * + * ALSA SoC I2S (McBSP) Audio Layer for TI DAVINCI processor + * Vladimir Barinov, <vbarinov@embeddedalley.com> + * Copyright (C) 2007 MontaVista Software, Inc., <source@mvista.com> + * + * OMAP ALSA SoC DAI driver using McBSP port + * Copyright (C) 2008 Nokia Corporation + * Contact: Jarkko Nikula <jarkko.nikula@bitmer.com> + * Peter Ujfalusi <peter.ujfalusi@ti.com> + * + * Freescale SSI ALSA SoC Digital Audio Interface (DAI) driver + * Author: Timur Tabi <timur@freescale.com> + * Copyright 2007-2010 Freescale Semiconductor, Inc. + */ + +#include <linux/bitops.h> +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_address.h> +#include <linux/slab.h> + +#include <sound/core.h> +#include <sound/dmaengine_pcm.h> +#include <sound/initval.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +/* I2S registers */ +#define BCM2835_I2S_CS_A_REG 0x00 +#define BCM2835_I2S_FIFO_A_REG 0x04 +#define BCM2835_I2S_MODE_A_REG 0x08 +#define BCM2835_I2S_RXC_A_REG 0x0c +#define BCM2835_I2S_TXC_A_REG 0x10 +#define BCM2835_I2S_DREQ_A_REG 0x14 +#define BCM2835_I2S_INTEN_A_REG 0x18 +#define BCM2835_I2S_INTSTC_A_REG 0x1c +#define BCM2835_I2S_GRAY_REG 0x20 + +/* I2S register settings */ +#define BCM2835_I2S_STBY BIT(25) +#define BCM2835_I2S_SYNC BIT(24) +#define BCM2835_I2S_RXSEX BIT(23) +#define BCM2835_I2S_RXF BIT(22) +#define BCM2835_I2S_TXE BIT(21) +#define BCM2835_I2S_RXD BIT(20) +#define BCM2835_I2S_TXD BIT(19) +#define BCM2835_I2S_RXR BIT(18) +#define BCM2835_I2S_TXW BIT(17) +#define BCM2835_I2S_CS_RXERR BIT(16) +#define BCM2835_I2S_CS_TXERR BIT(15) +#define BCM2835_I2S_RXSYNC BIT(14) +#define BCM2835_I2S_TXSYNC BIT(13) +#define BCM2835_I2S_DMAEN BIT(9) +#define BCM2835_I2S_RXTHR(v) ((v) << 7) +#define BCM2835_I2S_TXTHR(v) ((v) << 5) +#define BCM2835_I2S_RXCLR BIT(4) +#define BCM2835_I2S_TXCLR BIT(3) +#define BCM2835_I2S_TXON BIT(2) +#define BCM2835_I2S_RXON BIT(1) +#define BCM2835_I2S_EN (1) + +#define BCM2835_I2S_CLKDIS BIT(28) +#define BCM2835_I2S_PDMN BIT(27) +#define BCM2835_I2S_PDME BIT(26) +#define BCM2835_I2S_FRXP BIT(25) +#define BCM2835_I2S_FTXP BIT(24) +#define BCM2835_I2S_CLKM BIT(23) +#define BCM2835_I2S_CLKI BIT(22) +#define BCM2835_I2S_FSM BIT(21) +#define BCM2835_I2S_FSI BIT(20) +#define BCM2835_I2S_FLEN(v) ((v) << 10) +#define BCM2835_I2S_FSLEN(v) (v) + +#define BCM2835_I2S_CHWEX BIT(15) +#define BCM2835_I2S_CHEN BIT(14) +#define BCM2835_I2S_CHPOS(v) ((v) << 4) +#define BCM2835_I2S_CHWID(v) (v) +#define BCM2835_I2S_CH1(v) ((v) << 16) +#define BCM2835_I2S_CH2(v) (v) +#define BCM2835_I2S_CH1_POS(v) BCM2835_I2S_CH1(BCM2835_I2S_CHPOS(v)) +#define BCM2835_I2S_CH2_POS(v) BCM2835_I2S_CH2(BCM2835_I2S_CHPOS(v)) + +#define BCM2835_I2S_TX_PANIC(v) ((v) << 24) +#define BCM2835_I2S_RX_PANIC(v) ((v) << 16) +#define BCM2835_I2S_TX(v) ((v) << 8) +#define BCM2835_I2S_RX(v) (v) + +#define BCM2835_I2S_INT_RXERR BIT(3) +#define BCM2835_I2S_INT_TXERR BIT(2) +#define BCM2835_I2S_INT_RXR BIT(1) +#define BCM2835_I2S_INT_TXW BIT(0) + +/* Frame length register is 10 bit, maximum length 1024 */ +#define BCM2835_I2S_MAX_FRAME_LENGTH 1024 + +/* General device struct */ +struct bcm2835_i2s_dev { + struct device *dev; + struct snd_dmaengine_dai_dma_data dma_data[2]; + unsigned int fmt; + unsigned int tdm_slots; + unsigned int rx_mask; + unsigned int tx_mask; + unsigned int slot_width; + unsigned int frame_length; + + struct regmap *i2s_regmap; + struct clk *clk; + bool clk_prepared; + int clk_rate; +}; + +static void bcm2835_i2s_start_clock(struct bcm2835_i2s_dev *dev) +{ + unsigned int master = dev->fmt & SND_SOC_DAIFMT_MASTER_MASK; + + if (dev->clk_prepared) + return; + + switch (master) { + case SND_SOC_DAIFMT_CBS_CFS: + case SND_SOC_DAIFMT_CBS_CFM: + clk_prepare_enable(dev->clk); + dev->clk_prepared = true; + break; + default: + break; + } +} + +static void bcm2835_i2s_stop_clock(struct bcm2835_i2s_dev *dev) +{ + if (dev->clk_prepared) + clk_disable_unprepare(dev->clk); + dev->clk_prepared = false; +} + +static void bcm2835_i2s_clear_fifos(struct bcm2835_i2s_dev *dev, + bool tx, bool rx) +{ + int timeout = 1000; + uint32_t syncval; + uint32_t csreg; + uint32_t i2s_active_state; + bool clk_was_prepared; + uint32_t off; + uint32_t clr; + + off = tx ? BCM2835_I2S_TXON : 0; + off |= rx ? BCM2835_I2S_RXON : 0; + + clr = tx ? BCM2835_I2S_TXCLR : 0; + clr |= rx ? BCM2835_I2S_RXCLR : 0; + + /* Backup the current state */ + regmap_read(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, &csreg); + i2s_active_state = csreg & (BCM2835_I2S_RXON | BCM2835_I2S_TXON); + + /* Start clock if not running */ + clk_was_prepared = dev->clk_prepared; + if (!clk_was_prepared) + bcm2835_i2s_start_clock(dev); + + /* Stop I2S module */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, off, 0); + + /* + * Clear the FIFOs + * Requires at least 2 PCM clock cycles to take effect + */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, clr, clr); + + /* Wait for 2 PCM clock cycles */ + + /* + * Toggle the SYNC flag. After 2 PCM clock cycles it can be read back + * FIXME: This does not seem to work for slave mode! + */ + regmap_read(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, &syncval); + syncval &= BCM2835_I2S_SYNC; + + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_SYNC, ~syncval); + + /* Wait for the SYNC flag changing it's state */ + while (--timeout) { + regmap_read(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, &csreg); + if ((csreg & BCM2835_I2S_SYNC) != syncval) + break; + } + + if (!timeout) + dev_err(dev->dev, "I2S SYNC error!\n"); + + /* Stop clock if it was not running before */ + if (!clk_was_prepared) + bcm2835_i2s_stop_clock(dev); + + /* Restore I2S state */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_RXON | BCM2835_I2S_TXON, i2s_active_state); +} + +static int bcm2835_i2s_set_dai_fmt(struct snd_soc_dai *dai, + unsigned int fmt) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + dev->fmt = fmt; + return 0; +} + +static int bcm2835_i2s_set_dai_bclk_ratio(struct snd_soc_dai *dai, + unsigned int ratio) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + + if (!ratio) { + dev->tdm_slots = 0; + return 0; + } + + if (ratio > BCM2835_I2S_MAX_FRAME_LENGTH) + return -EINVAL; + + dev->tdm_slots = 2; + dev->rx_mask = 0x03; + dev->tx_mask = 0x03; + dev->slot_width = ratio / 2; + dev->frame_length = ratio; + + return 0; +} + +static int bcm2835_i2s_set_dai_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, unsigned int rx_mask, + int slots, int width) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + + if (slots) { + if (slots < 0 || width < 0) + return -EINVAL; + + /* Limit masks to available slots */ + rx_mask &= GENMASK(slots - 1, 0); + tx_mask &= GENMASK(slots - 1, 0); + + /* + * The driver is limited to 2-channel setups. + * Check that exactly 2 bits are set in the masks. + */ + if (hweight_long((unsigned long) rx_mask) != 2 + || hweight_long((unsigned long) tx_mask) != 2) + return -EINVAL; + + if (slots * width > BCM2835_I2S_MAX_FRAME_LENGTH) + return -EINVAL; + } + + dev->tdm_slots = slots; + + dev->rx_mask = rx_mask; + dev->tx_mask = tx_mask; + dev->slot_width = width; + dev->frame_length = slots * width; + + return 0; +} + +/* + * Convert logical slot number into physical slot number. + * + * If odd_offset is 0 sequential number is identical to logical number. + * This is used for DSP modes with slot numbering 0 1 2 3 ... + * + * Otherwise odd_offset defines the physical offset for odd numbered + * slots. This is used for I2S and left/right justified modes to + * translate from logical slot numbers 0 1 2 3 ... into physical slot + * numbers 0 2 ... 3 4 ... + */ +static int bcm2835_i2s_convert_slot(unsigned int slot, unsigned int odd_offset) +{ + if (!odd_offset) + return slot; + + if (slot & 1) + return (slot >> 1) + odd_offset; + + return slot >> 1; +} + +/* + * Calculate channel position from mask and slot width. + * + * Mask must contain exactly 2 set bits. + * Lowest set bit is channel 1 position, highest set bit channel 2. + * The constant offset is added to both channel positions. + * + * If odd_offset is > 0 slot positions are translated to + * I2S-style TDM slot numbering ( 0 2 ... 3 4 ...) with odd + * logical slot numbers starting at physical slot odd_offset. + */ +static void bcm2835_i2s_calc_channel_pos( + unsigned int *ch1_pos, unsigned int *ch2_pos, + unsigned int mask, unsigned int width, + unsigned int bit_offset, unsigned int odd_offset) +{ + *ch1_pos = bcm2835_i2s_convert_slot((ffs(mask) - 1), odd_offset) + * width + bit_offset; + *ch2_pos = bcm2835_i2s_convert_slot((fls(mask) - 1), odd_offset) + * width + bit_offset; +} + +static int bcm2835_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + unsigned int data_length, data_delay, framesync_length; + unsigned int slots, slot_width, odd_slot_offset; + int frame_length, bclk_rate; + unsigned int rx_mask, tx_mask; + unsigned int rx_ch1_pos, rx_ch2_pos, tx_ch1_pos, tx_ch2_pos; + unsigned int mode, format; + bool bit_clock_master = false; + bool frame_sync_master = false; + bool frame_start_falling_edge = false; + uint32_t csreg; + int ret = 0; + + /* + * If a stream is already enabled, + * the registers are already set properly. + */ + regmap_read(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, &csreg); + + if (csreg & (BCM2835_I2S_TXON | BCM2835_I2S_RXON)) + return 0; + + data_length = params_width(params); + data_delay = 0; + odd_slot_offset = 0; + mode = 0; + + if (dev->tdm_slots) { + slots = dev->tdm_slots; + slot_width = dev->slot_width; + frame_length = dev->frame_length; + rx_mask = dev->rx_mask; + tx_mask = dev->tx_mask; + bclk_rate = dev->frame_length * params_rate(params); + } else { + slots = 2; + slot_width = params_width(params); + rx_mask = 0x03; + tx_mask = 0x03; + + frame_length = snd_soc_params_to_frame_size(params); + if (frame_length < 0) + return frame_length; + + bclk_rate = snd_soc_params_to_bclk(params); + if (bclk_rate < 0) + return bclk_rate; + } + + /* Check if data fits into slots */ + if (data_length > slot_width) + return -EINVAL; + + /* Check if CPU is bit clock master */ + switch (dev->fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBS_CFS: + case SND_SOC_DAIFMT_CBS_CFM: + bit_clock_master = true; + break; + case SND_SOC_DAIFMT_CBM_CFS: + case SND_SOC_DAIFMT_CBM_CFM: + bit_clock_master = false; + break; + default: + return -EINVAL; + } + + /* Check if CPU is frame sync master */ + switch (dev->fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBS_CFS: + case SND_SOC_DAIFMT_CBM_CFS: + frame_sync_master = true; + break; + case SND_SOC_DAIFMT_CBS_CFM: + case SND_SOC_DAIFMT_CBM_CFM: + frame_sync_master = false; + break; + default: + return -EINVAL; + } + + /* Clock should only be set up here if CPU is clock master */ + if (bit_clock_master && + (!dev->clk_prepared || dev->clk_rate != bclk_rate)) { + if (dev->clk_prepared) + bcm2835_i2s_stop_clock(dev); + + if (dev->clk_rate != bclk_rate) { + ret = clk_set_rate(dev->clk, bclk_rate); + if (ret) + return ret; + dev->clk_rate = bclk_rate; + } + + bcm2835_i2s_start_clock(dev); + } + + /* Setup the frame format */ + format = BCM2835_I2S_CHEN; + + if (data_length >= 24) + format |= BCM2835_I2S_CHWEX; + + format |= BCM2835_I2S_CHWID((data_length-8)&0xf); + + /* CH2 format is the same as for CH1 */ + format = BCM2835_I2S_CH1(format) | BCM2835_I2S_CH2(format); + + switch (dev->fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + /* I2S mode needs an even number of slots */ + if (slots & 1) + return -EINVAL; + + /* + * Use I2S-style logical slot numbering: even slots + * are in first half of frame, odd slots in second half. + */ + odd_slot_offset = slots >> 1; + + /* MSB starts one cycle after frame start */ + data_delay = 1; + + /* Setup frame sync signal for 50% duty cycle */ + framesync_length = frame_length / 2; + frame_start_falling_edge = true; + break; + case SND_SOC_DAIFMT_LEFT_J: + if (slots & 1) + return -EINVAL; + + odd_slot_offset = slots >> 1; + data_delay = 0; + framesync_length = frame_length / 2; + frame_start_falling_edge = false; + break; + case SND_SOC_DAIFMT_RIGHT_J: + if (slots & 1) + return -EINVAL; + + /* Odd frame lengths aren't supported */ + if (frame_length & 1) + return -EINVAL; + + odd_slot_offset = slots >> 1; + data_delay = slot_width - data_length; + framesync_length = frame_length / 2; + frame_start_falling_edge = false; + break; + case SND_SOC_DAIFMT_DSP_A: + data_delay = 1; + framesync_length = 1; + frame_start_falling_edge = false; + break; + case SND_SOC_DAIFMT_DSP_B: + data_delay = 0; + framesync_length = 1; + frame_start_falling_edge = false; + break; + default: + return -EINVAL; + } + + bcm2835_i2s_calc_channel_pos(&rx_ch1_pos, &rx_ch2_pos, + rx_mask, slot_width, data_delay, odd_slot_offset); + bcm2835_i2s_calc_channel_pos(&tx_ch1_pos, &tx_ch2_pos, + tx_mask, slot_width, data_delay, odd_slot_offset); + + /* + * Transmitting data immediately after frame start, eg + * in left-justified or DSP mode A, only works stable + * if bcm2835 is the frame clock master. + */ + if ((!rx_ch1_pos || !tx_ch1_pos) && !frame_sync_master) + dev_warn(dev->dev, + "Unstable slave config detected, L/R may be swapped"); + + /* + * Set format for both streams. + * We cannot set another frame length + * (and therefore word length) anyway, + * so the format will be the same. + */ + regmap_write(dev->i2s_regmap, BCM2835_I2S_RXC_A_REG, + format + | BCM2835_I2S_CH1_POS(rx_ch1_pos) + | BCM2835_I2S_CH2_POS(rx_ch2_pos)); + regmap_write(dev->i2s_regmap, BCM2835_I2S_TXC_A_REG, + format + | BCM2835_I2S_CH1_POS(tx_ch1_pos) + | BCM2835_I2S_CH2_POS(tx_ch2_pos)); + + /* Setup the I2S mode */ + + if (data_length <= 16) { + /* + * Use frame packed mode (2 channels per 32 bit word) + * We cannot set another frame length in the second stream + * (and therefore word length) anyway, + * so the format will be the same. + */ + mode |= BCM2835_I2S_FTXP | BCM2835_I2S_FRXP; + } + + mode |= BCM2835_I2S_FLEN(frame_length - 1); + mode |= BCM2835_I2S_FSLEN(framesync_length); + + /* CLKM selects bcm2835 clock slave mode */ + if (!bit_clock_master) + mode |= BCM2835_I2S_CLKM; + + /* FSM selects bcm2835 frame sync slave mode */ + if (!frame_sync_master) + mode |= BCM2835_I2S_FSM; + + /* CLKI selects normal clocking mode, sampling on rising edge */ + switch (dev->fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + case SND_SOC_DAIFMT_NB_IF: + mode |= BCM2835_I2S_CLKI; + break; + case SND_SOC_DAIFMT_IB_NF: + case SND_SOC_DAIFMT_IB_IF: + break; + default: + return -EINVAL; + } + + /* FSI selects frame start on falling edge */ + switch (dev->fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + case SND_SOC_DAIFMT_IB_NF: + if (frame_start_falling_edge) + mode |= BCM2835_I2S_FSI; + break; + case SND_SOC_DAIFMT_NB_IF: + case SND_SOC_DAIFMT_IB_IF: + if (!frame_start_falling_edge) + mode |= BCM2835_I2S_FSI; + break; + default: + return -EINVAL; + } + + regmap_write(dev->i2s_regmap, BCM2835_I2S_MODE_A_REG, mode); + + /* Setup the DMA parameters */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_RXTHR(1) + | BCM2835_I2S_TXTHR(1) + | BCM2835_I2S_DMAEN, 0xffffffff); + + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_DREQ_A_REG, + BCM2835_I2S_TX_PANIC(0x10) + | BCM2835_I2S_RX_PANIC(0x30) + | BCM2835_I2S_TX(0x30) + | BCM2835_I2S_RX(0x20), 0xffffffff); + + /* Clear FIFOs */ + bcm2835_i2s_clear_fifos(dev, true, true); + + dev_dbg(dev->dev, + "slots: %d width: %d rx mask: 0x%02x tx_mask: 0x%02x\n", + slots, slot_width, rx_mask, tx_mask); + + dev_dbg(dev->dev, "frame len: %d sync len: %d data len: %d\n", + frame_length, framesync_length, data_length); + + dev_dbg(dev->dev, "rx pos: %d,%d tx pos: %d,%d\n", + rx_ch1_pos, rx_ch2_pos, tx_ch1_pos, tx_ch2_pos); + + dev_dbg(dev->dev, "sampling rate: %d bclk rate: %d\n", + params_rate(params), bclk_rate); + + dev_dbg(dev->dev, "CLKM: %d CLKI: %d FSM: %d FSI: %d frame start: %s edge\n", + !!(mode & BCM2835_I2S_CLKM), + !!(mode & BCM2835_I2S_CLKI), + !!(mode & BCM2835_I2S_FSM), + !!(mode & BCM2835_I2S_FSI), + (mode & BCM2835_I2S_FSI) ? "falling" : "rising"); + + return ret; +} + +static int bcm2835_i2s_prepare(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + uint32_t cs_reg; + + /* + * Clear both FIFOs if the one that should be started + * is not empty at the moment. This should only happen + * after overrun. Otherwise, hw_params would have cleared + * the FIFO. + */ + regmap_read(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, &cs_reg); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK + && !(cs_reg & BCM2835_I2S_TXE)) + bcm2835_i2s_clear_fifos(dev, true, false); + else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE + && (cs_reg & BCM2835_I2S_RXD)) + bcm2835_i2s_clear_fifos(dev, false, true); + + return 0; +} + +static void bcm2835_i2s_stop(struct bcm2835_i2s_dev *dev, + struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + uint32_t mask; + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + mask = BCM2835_I2S_RXON; + else + mask = BCM2835_I2S_TXON; + + regmap_update_bits(dev->i2s_regmap, + BCM2835_I2S_CS_A_REG, mask, 0); + + /* Stop also the clock when not SND_SOC_DAIFMT_CONT */ + if (!snd_soc_dai_active(dai) && !(dev->fmt & SND_SOC_DAIFMT_CONT)) + bcm2835_i2s_stop_clock(dev); +} + +static int bcm2835_i2s_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + uint32_t mask; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + bcm2835_i2s_start_clock(dev); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + mask = BCM2835_I2S_RXON; + else + mask = BCM2835_I2S_TXON; + + regmap_update_bits(dev->i2s_regmap, + BCM2835_I2S_CS_A_REG, mask, mask); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + bcm2835_i2s_stop(dev, substream, dai); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int bcm2835_i2s_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + + if (snd_soc_dai_active(dai)) + return 0; + + /* Should this still be running stop it */ + bcm2835_i2s_stop_clock(dev); + + /* Enable PCM block */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_EN, BCM2835_I2S_EN); + + /* + * Disable STBY. + * Requires at least 4 PCM clock cycles to take effect. + */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_STBY, BCM2835_I2S_STBY); + + return 0; +} + +static void bcm2835_i2s_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + + bcm2835_i2s_stop(dev, substream, dai); + + /* If both streams are stopped, disable module and clock */ + if (snd_soc_dai_active(dai)) + return; + + /* Disable the module */ + regmap_update_bits(dev->i2s_regmap, BCM2835_I2S_CS_A_REG, + BCM2835_I2S_EN, 0); + + /* + * Stopping clock is necessary, because stop does + * not stop the clock when SND_SOC_DAIFMT_CONT + */ + bcm2835_i2s_stop_clock(dev); +} + +static const struct snd_soc_dai_ops bcm2835_i2s_dai_ops = { + .startup = bcm2835_i2s_startup, + .shutdown = bcm2835_i2s_shutdown, + .prepare = bcm2835_i2s_prepare, + .trigger = bcm2835_i2s_trigger, + .hw_params = bcm2835_i2s_hw_params, + .set_fmt = bcm2835_i2s_set_dai_fmt, + .set_bclk_ratio = bcm2835_i2s_set_dai_bclk_ratio, + .set_tdm_slot = bcm2835_i2s_set_dai_tdm_slot, +}; + +static int bcm2835_i2s_dai_probe(struct snd_soc_dai *dai) +{ + struct bcm2835_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); + + snd_soc_dai_init_dma_data(dai, + &dev->dma_data[SNDRV_PCM_STREAM_PLAYBACK], + &dev->dma_data[SNDRV_PCM_STREAM_CAPTURE]); + + return 0; +} + +static struct snd_soc_dai_driver bcm2835_i2s_dai = { + .name = "bcm2835-i2s", + .probe = bcm2835_i2s_dai_probe, + .playback = { + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_CONTINUOUS, + .rate_min = 8000, + .rate_max = 384000, + .formats = SNDRV_PCM_FMTBIT_S16_LE + | SNDRV_PCM_FMTBIT_S24_LE + | SNDRV_PCM_FMTBIT_S32_LE + }, + .capture = { + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_CONTINUOUS, + .rate_min = 8000, + .rate_max = 384000, + .formats = SNDRV_PCM_FMTBIT_S16_LE + | SNDRV_PCM_FMTBIT_S24_LE + | SNDRV_PCM_FMTBIT_S32_LE + }, + .ops = &bcm2835_i2s_dai_ops, + .symmetric_rates = 1, + .symmetric_samplebits = 1, +}; + +static bool bcm2835_i2s_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case BCM2835_I2S_CS_A_REG: + case BCM2835_I2S_FIFO_A_REG: + case BCM2835_I2S_INTSTC_A_REG: + case BCM2835_I2S_GRAY_REG: + return true; + default: + return false; + }; +} + +static bool bcm2835_i2s_precious_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case BCM2835_I2S_FIFO_A_REG: + return true; + default: + return false; + }; +} + +static const struct regmap_config bcm2835_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .max_register = BCM2835_I2S_GRAY_REG, + .precious_reg = bcm2835_i2s_precious_reg, + .volatile_reg = bcm2835_i2s_volatile_reg, + .cache_type = REGCACHE_RBTREE, +}; + +static const struct snd_soc_component_driver bcm2835_i2s_component = { + .name = "bcm2835-i2s-comp", +}; + +static int bcm2835_i2s_probe(struct platform_device *pdev) +{ + struct bcm2835_i2s_dev *dev; + int ret; + void __iomem *base; + const __be32 *addr; + dma_addr_t dma_base; + + dev = devm_kzalloc(&pdev->dev, sizeof(*dev), + GFP_KERNEL); + if (!dev) + return -ENOMEM; + + /* get the clock */ + dev->clk_prepared = false; + dev->clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(dev->clk)) { + ret = PTR_ERR(dev->clk); + if (ret == -EPROBE_DEFER) + dev_dbg(&pdev->dev, "could not get clk: %d\n", ret); + else + dev_err(&pdev->dev, "could not get clk: %d\n", ret); + return ret; + } + + /* Request ioarea */ + base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(base)) + return PTR_ERR(base); + + dev->i2s_regmap = devm_regmap_init_mmio(&pdev->dev, base, + &bcm2835_regmap_config); + if (IS_ERR(dev->i2s_regmap)) + return PTR_ERR(dev->i2s_regmap); + + /* Set the DMA address - we have to parse DT ourselves */ + addr = of_get_address(pdev->dev.of_node, 0, NULL, NULL); + if (!addr) { + dev_err(&pdev->dev, "could not get DMA-register address\n"); + return -EINVAL; + } + dma_base = be32_to_cpup(addr); + + dev->dma_data[SNDRV_PCM_STREAM_PLAYBACK].addr = + dma_base + BCM2835_I2S_FIFO_A_REG; + + dev->dma_data[SNDRV_PCM_STREAM_CAPTURE].addr = + dma_base + BCM2835_I2S_FIFO_A_REG; + + /* Set the bus width */ + dev->dma_data[SNDRV_PCM_STREAM_PLAYBACK].addr_width = + DMA_SLAVE_BUSWIDTH_4_BYTES; + dev->dma_data[SNDRV_PCM_STREAM_CAPTURE].addr_width = + DMA_SLAVE_BUSWIDTH_4_BYTES; + + /* Set burst */ + dev->dma_data[SNDRV_PCM_STREAM_PLAYBACK].maxburst = 2; + dev->dma_data[SNDRV_PCM_STREAM_CAPTURE].maxburst = 2; + + /* + * Set the PACK flag to enable S16_LE support (2 S16_LE values + * packed into 32-bit transfers). + */ + dev->dma_data[SNDRV_PCM_STREAM_PLAYBACK].flags = + SND_DMAENGINE_PCM_DAI_FLAG_PACK; + dev->dma_data[SNDRV_PCM_STREAM_CAPTURE].flags = + SND_DMAENGINE_PCM_DAI_FLAG_PACK; + + /* Store the pdev */ + dev->dev = &pdev->dev; + dev_set_drvdata(&pdev->dev, dev); + + ret = devm_snd_soc_register_component(&pdev->dev, + &bcm2835_i2s_component, &bcm2835_i2s_dai, 1); + if (ret) { + dev_err(&pdev->dev, "Could not register DAI: %d\n", ret); + return ret; + } + + ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0); + if (ret) { + dev_err(&pdev->dev, "Could not register PCM: %d\n", ret); + return ret; + } + + return 0; +} + +static const struct of_device_id bcm2835_i2s_of_match[] = { + { .compatible = "brcm,bcm2835-i2s", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, bcm2835_i2s_of_match); + +static struct platform_driver bcm2835_i2s_driver = { + .probe = bcm2835_i2s_probe, + .driver = { + .name = "bcm2835-i2s", + .of_match_table = bcm2835_i2s_of_match, + }, +}; + +module_platform_driver(bcm2835_i2s_driver); + +MODULE_ALIAS("platform:bcm2835-i2s"); +MODULE_DESCRIPTION("BCM2835 I2S interface"); +MODULE_AUTHOR("Florian Meier <florian.meier@koalo.de>"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/bcm/bcm63xx-i2s-whistler.c b/sound/soc/bcm/bcm63xx-i2s-whistler.c new file mode 100644 index 000000000..246a57ac6 --- /dev/null +++ b/sound/soc/bcm/bcm63xx-i2s-whistler.c @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// linux/sound/bcm/bcm63xx-i2s-whistler.c +// BCM63xx whistler i2s driver +// Copyright (c) 2020 Broadcom Corporation +// Author: Kevin-Ke Li <kevin-ke.li@broadcom.com> + +#include <linux/clk.h> +#include <linux/dma-mapping.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/regmap.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include "bcm63xx-i2s.h" + +#define DRV_NAME "brcm-i2s" + +static bool brcm_i2s_wr_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case I2S_TX_CFG ... I2S_TX_DESC_IFF_LEN: + case I2S_TX_CFG_2 ... I2S_RX_DESC_IFF_LEN: + case I2S_RX_CFG_2 ... I2S_REG_MAX: + return true; + default: + return false; + } +} + +static bool brcm_i2s_rd_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case I2S_TX_CFG ... I2S_REG_MAX: + return true; + default: + return false; + } +} + +static bool brcm_i2s_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case I2S_TX_CFG: + case I2S_TX_IRQ_CTL: + case I2S_TX_DESC_IFF_ADDR: + case I2S_TX_DESC_IFF_LEN: + case I2S_TX_DESC_OFF_ADDR: + case I2S_TX_DESC_OFF_LEN: + case I2S_TX_CFG_2: + case I2S_RX_CFG: + case I2S_RX_IRQ_CTL: + case I2S_RX_DESC_OFF_ADDR: + case I2S_RX_DESC_OFF_LEN: + case I2S_RX_DESC_IFF_LEN: + case I2S_RX_DESC_IFF_ADDR: + case I2S_RX_CFG_2: + return true; + default: + return false; + } +} + +static const struct regmap_config brcm_i2s_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .max_register = I2S_REG_MAX, + .writeable_reg = brcm_i2s_wr_reg, + .readable_reg = brcm_i2s_rd_reg, + .volatile_reg = brcm_i2s_volatile_reg, + .cache_type = REGCACHE_FLAT, +}; + +static int bcm63xx_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + int ret = 0; + struct bcm_i2s_priv *i2s_priv = snd_soc_dai_get_drvdata(dai); + + ret = clk_set_rate(i2s_priv->i2s_clk, params_rate(params)); + if (ret < 0) + dev_err(i2s_priv->dev, + "Can't set sample rate, err: %d\n", ret); + + return ret; +} + +static int bcm63xx_i2s_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + unsigned int slavemode; + struct bcm_i2s_priv *i2s_priv = snd_soc_dai_get_drvdata(dai); + struct regmap *regmap_i2s = i2s_priv->regmap_i2s; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + regmap_update_bits(regmap_i2s, I2S_TX_CFG, + I2S_TX_OUT_R | I2S_TX_DATA_ALIGNMENT | + I2S_TX_DATA_ENABLE | I2S_TX_CLOCK_ENABLE, + I2S_TX_OUT_R | I2S_TX_DATA_ALIGNMENT | + I2S_TX_DATA_ENABLE | I2S_TX_CLOCK_ENABLE); + regmap_write(regmap_i2s, I2S_TX_IRQ_CTL, 0); + regmap_write(regmap_i2s, I2S_TX_IRQ_IFF_THLD, 0); + regmap_write(regmap_i2s, I2S_TX_IRQ_OFF_THLD, 1); + + /* TX and RX block each have an independent bit to indicate + * if it is generating the clock for the I2S bus. The bus + * clocks need to be generated from either the TX or RX block, + * but not both + */ + regmap_read(regmap_i2s, I2S_RX_CFG_2, &slavemode); + if (slavemode & I2S_RX_SLAVE_MODE_MASK) + regmap_update_bits(regmap_i2s, I2S_TX_CFG_2, + I2S_TX_SLAVE_MODE_MASK, + I2S_TX_MASTER_MODE); + else + regmap_update_bits(regmap_i2s, I2S_TX_CFG_2, + I2S_TX_SLAVE_MODE_MASK, + I2S_TX_SLAVE_MODE); + } else { + regmap_update_bits(regmap_i2s, I2S_RX_CFG, + I2S_RX_IN_R | I2S_RX_DATA_ALIGNMENT | + I2S_RX_CLOCK_ENABLE, + I2S_RX_IN_R | I2S_RX_DATA_ALIGNMENT | + I2S_RX_CLOCK_ENABLE); + regmap_write(regmap_i2s, I2S_RX_IRQ_CTL, 0); + regmap_write(regmap_i2s, I2S_RX_IRQ_IFF_THLD, 0); + regmap_write(regmap_i2s, I2S_RX_IRQ_OFF_THLD, 1); + + regmap_read(regmap_i2s, I2S_TX_CFG_2, &slavemode); + if (slavemode & I2S_TX_SLAVE_MODE_MASK) + regmap_update_bits(regmap_i2s, I2S_RX_CFG_2, + I2S_RX_SLAVE_MODE_MASK, 0); + else + regmap_update_bits(regmap_i2s, I2S_RX_CFG_2, + I2S_RX_SLAVE_MODE_MASK, + I2S_RX_SLAVE_MODE); + } + return 0; +} + +static void bcm63xx_i2s_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + unsigned int enabled, slavemode; + struct bcm_i2s_priv *i2s_priv = snd_soc_dai_get_drvdata(dai); + struct regmap *regmap_i2s = i2s_priv->regmap_i2s; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + regmap_update_bits(regmap_i2s, I2S_TX_CFG, + I2S_TX_OUT_R | I2S_TX_DATA_ALIGNMENT | + I2S_TX_DATA_ENABLE | I2S_TX_CLOCK_ENABLE, 0); + regmap_write(regmap_i2s, I2S_TX_IRQ_CTL, 1); + regmap_write(regmap_i2s, I2S_TX_IRQ_IFF_THLD, 4); + regmap_write(regmap_i2s, I2S_TX_IRQ_OFF_THLD, 4); + + regmap_read(regmap_i2s, I2S_TX_CFG_2, &slavemode); + slavemode = slavemode & I2S_TX_SLAVE_MODE_MASK; + if (!slavemode) { + regmap_read(regmap_i2s, I2S_RX_CFG, &enabled); + enabled = enabled & I2S_RX_ENABLE_MASK; + if (enabled) + regmap_update_bits(regmap_i2s, I2S_RX_CFG_2, + I2S_RX_SLAVE_MODE_MASK, + I2S_RX_MASTER_MODE); + } + regmap_update_bits(regmap_i2s, I2S_TX_CFG_2, + I2S_TX_SLAVE_MODE_MASK, + I2S_TX_SLAVE_MODE); + } else { + regmap_update_bits(regmap_i2s, I2S_RX_CFG, + I2S_RX_IN_R | I2S_RX_DATA_ALIGNMENT | + I2S_RX_CLOCK_ENABLE, 0); + regmap_write(regmap_i2s, I2S_RX_IRQ_CTL, 1); + regmap_write(regmap_i2s, I2S_RX_IRQ_IFF_THLD, 4); + regmap_write(regmap_i2s, I2S_RX_IRQ_OFF_THLD, 4); + + regmap_read(regmap_i2s, I2S_RX_CFG_2, &slavemode); + slavemode = slavemode & I2S_RX_SLAVE_MODE_MASK; + if (!slavemode) { + regmap_read(regmap_i2s, I2S_TX_CFG, &enabled); + enabled = enabled & I2S_TX_ENABLE_MASK; + if (enabled) + regmap_update_bits(regmap_i2s, I2S_TX_CFG_2, + I2S_TX_SLAVE_MODE_MASK, + I2S_TX_MASTER_MODE); + } + + regmap_update_bits(regmap_i2s, I2S_RX_CFG_2, + I2S_RX_SLAVE_MODE_MASK, I2S_RX_SLAVE_MODE); + } +} + +static const struct snd_soc_dai_ops bcm63xx_i2s_dai_ops = { + .startup = bcm63xx_i2s_startup, + .shutdown = bcm63xx_i2s_shutdown, + .hw_params = bcm63xx_i2s_hw_params, +}; + +static struct snd_soc_dai_driver bcm63xx_i2s_dai = { + .name = DRV_NAME, + .playback = { + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_192000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + }, + .capture = { + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_192000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + }, + .ops = &bcm63xx_i2s_dai_ops, + .symmetric_rates = 1, + .symmetric_channels = 1, +}; + +static const struct snd_soc_component_driver bcm63xx_i2s_component = { + .name = "bcm63xx", +}; + +static int bcm63xx_i2s_dev_probe(struct platform_device *pdev) +{ + int ret = 0; + void __iomem *regs; + struct resource *r_mem, *region; + struct bcm_i2s_priv *i2s_priv; + struct regmap *regmap_i2s; + struct clk *i2s_clk; + + i2s_priv = devm_kzalloc(&pdev->dev, sizeof(*i2s_priv), GFP_KERNEL); + if (!i2s_priv) + return -ENOMEM; + + i2s_clk = devm_clk_get(&pdev->dev, "i2sclk"); + if (IS_ERR(i2s_clk)) { + dev_err(&pdev->dev, "%s: cannot get a brcm clock: %ld\n", + __func__, PTR_ERR(i2s_clk)); + return PTR_ERR(i2s_clk); + } + + r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!r_mem) { + dev_err(&pdev->dev, "Unable to get register resource.\n"); + return -ENODEV; + } + + region = devm_request_mem_region(&pdev->dev, r_mem->start, + resource_size(r_mem), DRV_NAME); + if (!region) { + dev_err(&pdev->dev, "Memory region already claimed\n"); + return -EBUSY; + } + + regs = devm_ioremap_resource(&pdev->dev, r_mem); + if (IS_ERR(regs)) { + ret = PTR_ERR(regs); + return ret; + } + + regmap_i2s = devm_regmap_init_mmio(&pdev->dev, + regs, &brcm_i2s_regmap_config); + if (IS_ERR(regmap_i2s)) + return PTR_ERR(regmap_i2s); + + regmap_update_bits(regmap_i2s, I2S_MISC_CFG, + I2S_PAD_LVL_LOOP_DIS_MASK, + I2S_PAD_LVL_LOOP_DIS_ENABLE); + + ret = devm_snd_soc_register_component(&pdev->dev, + &bcm63xx_i2s_component, + &bcm63xx_i2s_dai, 1); + if (ret) { + dev_err(&pdev->dev, "failed to register the dai\n"); + return ret; + } + + i2s_priv->dev = &pdev->dev; + i2s_priv->i2s_clk = i2s_clk; + i2s_priv->regmap_i2s = regmap_i2s; + dev_set_drvdata(&pdev->dev, i2s_priv); + + ret = bcm63xx_soc_platform_probe(pdev, i2s_priv); + if (ret) + dev_err(&pdev->dev, "failed to register the pcm\n"); + + return ret; +} + +static int bcm63xx_i2s_dev_remove(struct platform_device *pdev) +{ + bcm63xx_soc_platform_remove(pdev); + return 0; +} + +#ifdef CONFIG_OF +static const struct of_device_id snd_soc_bcm_audio_match[] = { + {.compatible = "brcm,bcm63xx-i2s"}, + { } +}; +#endif + +static struct platform_driver bcm63xx_i2s_driver = { + .driver = { + .name = DRV_NAME, + .of_match_table = of_match_ptr(snd_soc_bcm_audio_match), + }, + .probe = bcm63xx_i2s_dev_probe, + .remove = bcm63xx_i2s_dev_remove, +}; + +module_platform_driver(bcm63xx_i2s_driver); + +MODULE_AUTHOR("Kevin,Li <kevin-ke.li@broadcom.com>"); +MODULE_DESCRIPTION("Broadcom DSL XPON ASOC I2S Interface"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/bcm/bcm63xx-i2s.h b/sound/soc/bcm/bcm63xx-i2s.h new file mode 100644 index 000000000..edc328ba5 --- /dev/null +++ b/sound/soc/bcm/bcm63xx-i2s.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// linux/sound/soc/bcm/bcm63xx-i2s.h +// Copyright (c) 2020 Broadcom Corporation +// Author: Kevin-Ke Li <kevin-ke.li@broadcom.com> + +#ifndef __BCM63XX_I2S_H +#define __BCM63XX_I2S_H + +#define I2S_DESC_FIFO_DEPTH 8 +#define I2S_MISC_CFG (0x003C) +#define I2S_PAD_LVL_LOOP_DIS_MASK (1 << 2) +#define I2S_PAD_LVL_LOOP_DIS_ENABLE I2S_PAD_LVL_LOOP_DIS_MASK + +#define I2S_TX_ENABLE_MASK (1 << 31) +#define I2S_TX_ENABLE I2S_TX_ENABLE_MASK +#define I2S_TX_OUT_R (1 << 19) +#define I2S_TX_DATA_ALIGNMENT (1 << 2) +#define I2S_TX_DATA_ENABLE (1 << 1) +#define I2S_TX_CLOCK_ENABLE (1 << 0) + +#define I2S_TX_DESC_OFF_LEVEL_SHIFT 12 +#define I2S_TX_DESC_OFF_LEVEL_MASK (0x0F << I2S_TX_DESC_OFF_LEVEL_SHIFT) +#define I2S_TX_DESC_IFF_LEVEL_SHIFT 8 +#define I2S_TX_DESC_IFF_LEVEL_MASK (0x0F << I2S_TX_DESC_IFF_LEVEL_SHIFT) +#define I2S_TX_DESC_OFF_INTR_EN_MSK (1 << 1) +#define I2S_TX_DESC_OFF_INTR_EN I2S_TX_DESC_OFF_INTR_EN_MSK + +#define I2S_TX_CFG (0x0000) +#define I2S_TX_IRQ_CTL (0x0004) +#define I2S_TX_IRQ_EN (0x0008) +#define I2S_TX_IRQ_IFF_THLD (0x000c) +#define I2S_TX_IRQ_OFF_THLD (0x0010) +#define I2S_TX_DESC_IFF_ADDR (0x0014) +#define I2S_TX_DESC_IFF_LEN (0x0018) +#define I2S_TX_DESC_OFF_ADDR (0x001C) +#define I2S_TX_DESC_OFF_LEN (0x0020) +#define I2S_TX_CFG_2 (0x0024) +#define I2S_TX_SLAVE_MODE_SHIFT 13 +#define I2S_TX_SLAVE_MODE_MASK (1 << I2S_TX_SLAVE_MODE_SHIFT) +#define I2S_TX_SLAVE_MODE I2S_TX_SLAVE_MODE_MASK +#define I2S_TX_MASTER_MODE 0 +#define I2S_TX_INTR_MASK 0x0F + +#define I2S_RX_ENABLE_MASK (1 << 31) +#define I2S_RX_ENABLE I2S_RX_ENABLE_MASK +#define I2S_RX_IN_R (1 << 19) +#define I2S_RX_DATA_ALIGNMENT (1 << 2) +#define I2S_RX_CLOCK_ENABLE (1 << 0) + +#define I2S_RX_DESC_OFF_LEVEL_SHIFT 12 +#define I2S_RX_DESC_OFF_LEVEL_MASK (0x0F << I2S_RX_DESC_OFF_LEVEL_SHIFT) +#define I2S_RX_DESC_IFF_LEVEL_SHIFT 8 +#define I2S_RX_DESC_IFF_LEVEL_MASK (0x0F << I2S_RX_DESC_IFF_LEVEL_SHIFT) +#define I2S_RX_DESC_OFF_INTR_EN_MSK (1 << 1) +#define I2S_RX_DESC_OFF_INTR_EN I2S_RX_DESC_OFF_INTR_EN_MSK + +#define I2S_RX_CFG (0x0040) /* 20c0 */ +#define I2S_RX_IRQ_CTL (0x0044) +#define I2S_RX_IRQ_EN (0x0048) +#define I2S_RX_IRQ_IFF_THLD (0x004C) +#define I2S_RX_IRQ_OFF_THLD (0x0050) +#define I2S_RX_DESC_IFF_ADDR (0x0054) +#define I2S_RX_DESC_IFF_LEN (0x0058) +#define I2S_RX_DESC_OFF_ADDR (0x005C) +#define I2S_RX_DESC_OFF_LEN (0x0060) +#define I2S_RX_CFG_2 (0x0064) +#define I2S_RX_SLAVE_MODE_SHIFT 13 +#define I2S_RX_SLAVE_MODE_MASK (1 << I2S_RX_SLAVE_MODE_SHIFT) +#define I2S_RX_SLAVE_MODE I2S_RX_SLAVE_MODE_MASK +#define I2S_RX_MASTER_MODE 0 +#define I2S_RX_INTR_MASK 0x0F + +#define I2S_REG_MAX 0x007C + +struct bcm_i2s_priv { + struct device *dev; + struct resource *r_irq; + struct regmap *regmap_i2s; + struct clk *i2s_clk; + struct snd_pcm_substream *play_substream; + struct snd_pcm_substream *capture_substream; + struct i2s_dma_desc *play_dma_desc; + struct i2s_dma_desc *capture_dma_desc; +}; + +extern int bcm63xx_soc_platform_probe(struct platform_device *pdev, + struct bcm_i2s_priv *i2s_priv); +extern int bcm63xx_soc_platform_remove(struct platform_device *pdev); + +#endif diff --git a/sound/soc/bcm/bcm63xx-pcm-whistler.c b/sound/soc/bcm/bcm63xx-pcm-whistler.c new file mode 100644 index 000000000..7ec8559d5 --- /dev/null +++ b/sound/soc/bcm/bcm63xx-pcm-whistler.c @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// linux/sound/bcm/bcm63xx-pcm-whistler.c +// BCM63xx whistler pcm interface +// Copyright (c) 2020 Broadcom Corporation +// Author: Kevin-Ke Li <kevin-ke.li@broadcom.com> + +#include <linux/dma-mapping.h> +#include <linux/io.h> +#include <linux/module.h> +#include <sound/pcm_params.h> +#include <linux/regmap.h> +#include <linux/of_device.h> +#include <sound/soc.h> +#include "bcm63xx-i2s.h" + + +struct i2s_dma_desc { + unsigned char *dma_area; + dma_addr_t dma_addr; + unsigned int dma_len; +}; + +struct bcm63xx_runtime_data { + int dma_len; + dma_addr_t dma_addr; + dma_addr_t dma_addr_next; +}; + +static const struct snd_pcm_hardware bcm63xx_pcm_hardware = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_RESUME, + .formats = SNDRV_PCM_FMTBIT_S32_LE, /* support S32 only */ + .period_bytes_max = 8192 - 32, + .periods_min = 1, + .periods_max = PAGE_SIZE/sizeof(struct i2s_dma_desc), + .buffer_bytes_max = 128 * 1024, + .fifo_size = 32, +}; + +static int bcm63xx_pcm_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct i2s_dma_desc *dma_desc; + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); + runtime->dma_bytes = params_buffer_bytes(params); + + dma_desc = kzalloc(sizeof(*dma_desc), GFP_NOWAIT); + if (!dma_desc) + return -ENOMEM; + + snd_soc_dai_set_dma_data(asoc_rtd_to_cpu(rtd, 0), substream, dma_desc); + + return 0; +} + +static int bcm63xx_pcm_hw_free(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct i2s_dma_desc *dma_desc; + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + + dma_desc = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); + kfree(dma_desc); + snd_pcm_set_runtime_buffer(substream, NULL); + + return 0; +} + +static int bcm63xx_pcm_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + int ret = 0; + struct snd_soc_pcm_runtime *rtd; + struct bcm_i2s_priv *i2s_priv; + struct regmap *regmap_i2s; + + rtd = asoc_substream_to_rtd(substream); + i2s_priv = dev_get_drvdata(asoc_rtd_to_cpu(rtd, 0)->dev); + regmap_i2s = i2s_priv->regmap_i2s; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + regmap_update_bits(regmap_i2s, + I2S_TX_IRQ_EN, + I2S_TX_DESC_OFF_INTR_EN, + I2S_TX_DESC_OFF_INTR_EN); + regmap_update_bits(regmap_i2s, + I2S_TX_CFG, + I2S_TX_ENABLE_MASK, + I2S_TX_ENABLE); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + regmap_write(regmap_i2s, + I2S_TX_IRQ_EN, + 0); + regmap_update_bits(regmap_i2s, + I2S_TX_CFG, + I2S_TX_ENABLE_MASK, + 0); + break; + default: + ret = -EINVAL; + } + } else { + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + regmap_update_bits(regmap_i2s, + I2S_RX_IRQ_EN, + I2S_RX_DESC_OFF_INTR_EN_MSK, + I2S_RX_DESC_OFF_INTR_EN); + regmap_update_bits(regmap_i2s, + I2S_RX_CFG, + I2S_RX_ENABLE_MASK, + I2S_RX_ENABLE); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + regmap_update_bits(regmap_i2s, + I2S_RX_IRQ_EN, + I2S_RX_DESC_OFF_INTR_EN_MSK, + 0); + regmap_update_bits(regmap_i2s, + I2S_RX_CFG, + I2S_RX_ENABLE_MASK, + 0); + break; + default: + ret = -EINVAL; + } + } + return ret; +} + +static int bcm63xx_pcm_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct i2s_dma_desc *dma_desc; + struct regmap *regmap_i2s; + struct bcm_i2s_priv *i2s_priv; + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + uint32_t regaddr_desclen, regaddr_descaddr; + + dma_desc = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); + dma_desc->dma_len = snd_pcm_lib_period_bytes(substream); + dma_desc->dma_addr = runtime->dma_addr; + dma_desc->dma_area = runtime->dma_area; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + regaddr_desclen = I2S_TX_DESC_IFF_LEN; + regaddr_descaddr = I2S_TX_DESC_IFF_ADDR; + } else { + regaddr_desclen = I2S_RX_DESC_IFF_LEN; + regaddr_descaddr = I2S_RX_DESC_IFF_ADDR; + } + + i2s_priv = dev_get_drvdata(asoc_rtd_to_cpu(rtd, 0)->dev); + regmap_i2s = i2s_priv->regmap_i2s; + + regmap_write(regmap_i2s, regaddr_desclen, dma_desc->dma_len); + regmap_write(regmap_i2s, regaddr_descaddr, dma_desc->dma_addr); + + return 0; +} + +static snd_pcm_uframes_t +bcm63xx_pcm_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + snd_pcm_uframes_t x; + struct bcm63xx_runtime_data *prtd = substream->runtime->private_data; + + if (!prtd->dma_addr_next) + prtd->dma_addr_next = substream->runtime->dma_addr; + + x = bytes_to_frames(substream->runtime, + prtd->dma_addr_next - substream->runtime->dma_addr); + + return x == substream->runtime->buffer_size ? 0 : x; +} + +static int bcm63xx_pcm_mmap(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct vm_area_struct *vma) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + + return dma_mmap_wc(substream->pcm->card->dev, vma, + runtime->dma_area, + runtime->dma_addr, + runtime->dma_bytes); + +} + +static int bcm63xx_pcm_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + int ret = 0; + struct snd_pcm_runtime *runtime = substream->runtime; + struct bcm63xx_runtime_data *prtd; + + runtime->hw = bcm63xx_pcm_hardware; + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 32); + if (ret) + goto out; + + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 32); + if (ret) + goto out; + + ret = snd_pcm_hw_constraint_integer(runtime, + SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + goto out; + + ret = -ENOMEM; + prtd = kzalloc(sizeof(*prtd), GFP_KERNEL); + if (!prtd) + goto out; + + runtime->private_data = prtd; + return 0; +out: + return ret; +} + +static int bcm63xx_pcm_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct bcm63xx_runtime_data *prtd = runtime->private_data; + + kfree(prtd); + return 0; +} + +static irqreturn_t i2s_dma_isr(int irq, void *bcm_i2s_priv) +{ + unsigned int availdepth, ifflevel, offlevel, int_status, val_1, val_2; + struct bcm63xx_runtime_data *prtd; + struct snd_pcm_substream *substream; + struct snd_pcm_runtime *runtime; + struct regmap *regmap_i2s; + struct i2s_dma_desc *dma_desc; + struct snd_soc_pcm_runtime *rtd; + struct bcm_i2s_priv *i2s_priv; + + i2s_priv = (struct bcm_i2s_priv *)bcm_i2s_priv; + regmap_i2s = i2s_priv->regmap_i2s; + + /* rx */ + regmap_read(regmap_i2s, I2S_RX_IRQ_CTL, &int_status); + + if (int_status & I2S_RX_DESC_OFF_INTR_EN_MSK) { + substream = i2s_priv->capture_substream; + runtime = substream->runtime; + rtd = asoc_substream_to_rtd(substream); + prtd = runtime->private_data; + dma_desc = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); + + offlevel = (int_status & I2S_RX_DESC_OFF_LEVEL_MASK) >> + I2S_RX_DESC_OFF_LEVEL_SHIFT; + while (offlevel) { + regmap_read(regmap_i2s, I2S_RX_DESC_OFF_ADDR, &val_1); + regmap_read(regmap_i2s, I2S_RX_DESC_OFF_LEN, &val_2); + offlevel--; + } + prtd->dma_addr_next = val_1 + val_2; + ifflevel = (int_status & I2S_RX_DESC_IFF_LEVEL_MASK) >> + I2S_RX_DESC_IFF_LEVEL_SHIFT; + + availdepth = I2S_DESC_FIFO_DEPTH - ifflevel; + while (availdepth) { + dma_desc->dma_addr += + snd_pcm_lib_period_bytes(substream); + dma_desc->dma_area += + snd_pcm_lib_period_bytes(substream); + if (dma_desc->dma_addr - runtime->dma_addr >= + runtime->dma_bytes) { + dma_desc->dma_addr = runtime->dma_addr; + dma_desc->dma_area = runtime->dma_area; + } + + prtd->dma_addr = dma_desc->dma_addr; + regmap_write(regmap_i2s, I2S_RX_DESC_IFF_LEN, + snd_pcm_lib_period_bytes(substream)); + regmap_write(regmap_i2s, I2S_RX_DESC_IFF_ADDR, + dma_desc->dma_addr); + availdepth--; + } + + snd_pcm_period_elapsed(substream); + + /* Clear interrupt by writing 0 */ + regmap_update_bits(regmap_i2s, I2S_RX_IRQ_CTL, + I2S_RX_INTR_MASK, 0); + } + + /* tx */ + regmap_read(regmap_i2s, I2S_TX_IRQ_CTL, &int_status); + + if (int_status & I2S_TX_DESC_OFF_INTR_EN_MSK) { + substream = i2s_priv->play_substream; + runtime = substream->runtime; + rtd = asoc_substream_to_rtd(substream); + prtd = runtime->private_data; + dma_desc = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); + + offlevel = (int_status & I2S_TX_DESC_OFF_LEVEL_MASK) >> + I2S_TX_DESC_OFF_LEVEL_SHIFT; + while (offlevel) { + regmap_read(regmap_i2s, I2S_TX_DESC_OFF_ADDR, &val_1); + regmap_read(regmap_i2s, I2S_TX_DESC_OFF_LEN, &val_2); + prtd->dma_addr_next = val_1 + val_2; + offlevel--; + } + + ifflevel = (int_status & I2S_TX_DESC_IFF_LEVEL_MASK) >> + I2S_TX_DESC_IFF_LEVEL_SHIFT; + availdepth = I2S_DESC_FIFO_DEPTH - ifflevel; + + while (availdepth) { + dma_desc->dma_addr += + snd_pcm_lib_period_bytes(substream); + dma_desc->dma_area += + snd_pcm_lib_period_bytes(substream); + + if (dma_desc->dma_addr - runtime->dma_addr >= + runtime->dma_bytes) { + dma_desc->dma_addr = runtime->dma_addr; + dma_desc->dma_area = runtime->dma_area; + } + + prtd->dma_addr = dma_desc->dma_addr; + regmap_write(regmap_i2s, I2S_TX_DESC_IFF_LEN, + snd_pcm_lib_period_bytes(substream)); + regmap_write(regmap_i2s, I2S_TX_DESC_IFF_ADDR, + dma_desc->dma_addr); + availdepth--; + } + + snd_pcm_period_elapsed(substream); + + /* Clear interrupt by writing 0 */ + regmap_update_bits(regmap_i2s, I2S_TX_IRQ_CTL, + I2S_TX_INTR_MASK, 0); + } + + return IRQ_HANDLED; +} + +static int bcm63xx_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, int stream) +{ + struct snd_pcm_substream *substream = pcm->streams[stream].substream; + struct snd_dma_buffer *buf = &substream->dma_buffer; + size_t size = bcm63xx_pcm_hardware.buffer_bytes_max; + + buf->dev.type = SNDRV_DMA_TYPE_DEV; + buf->dev.dev = pcm->card->dev; + buf->private_data = NULL; + + buf->area = dma_alloc_wc(pcm->card->dev, + size, &buf->addr, + GFP_KERNEL); + if (!buf->area) + return -ENOMEM; + buf->bytes = size; + return 0; +} + +static int bcm63xx_soc_pcm_new(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct snd_pcm *pcm = rtd->pcm; + struct bcm_i2s_priv *i2s_priv; + int ret; + + i2s_priv = dev_get_drvdata(asoc_rtd_to_cpu(rtd, 0)->dev); + + of_dma_configure(pcm->card->dev, pcm->card->dev->of_node, 1); + + ret = dma_coerce_mask_and_coherent(pcm->card->dev, DMA_BIT_MASK(32)); + if (ret) + goto out; + + if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) { + ret = bcm63xx_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_PLAYBACK); + if (ret) + goto out; + + i2s_priv->play_substream = + pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; + } + + if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) { + ret = bcm63xx_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_CAPTURE); + if (ret) + goto out; + i2s_priv->capture_substream = + pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; + } + +out: + return ret; +} + +static void bcm63xx_pcm_free_dma_buffers(struct snd_soc_component *component, + struct snd_pcm *pcm) +{ + int stream; + struct snd_dma_buffer *buf; + struct snd_pcm_substream *substream; + + for (stream = 0; stream < 2; stream++) { + substream = pcm->streams[stream].substream; + if (!substream) + continue; + buf = &substream->dma_buffer; + if (!buf->area) + continue; + dma_free_wc(pcm->card->dev, buf->bytes, + buf->area, buf->addr); + buf->area = NULL; + } +} + +static const struct snd_soc_component_driver bcm63xx_soc_platform = { + .open = bcm63xx_pcm_open, + .close = bcm63xx_pcm_close, + .hw_params = bcm63xx_pcm_hw_params, + .hw_free = bcm63xx_pcm_hw_free, + .prepare = bcm63xx_pcm_prepare, + .trigger = bcm63xx_pcm_trigger, + .pointer = bcm63xx_pcm_pointer, + .mmap = bcm63xx_pcm_mmap, + .pcm_construct = bcm63xx_soc_pcm_new, + .pcm_destruct = bcm63xx_pcm_free_dma_buffers, +}; + +int bcm63xx_soc_platform_probe(struct platform_device *pdev, + struct bcm_i2s_priv *i2s_priv) +{ + int ret; + + i2s_priv->r_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0); + if (!i2s_priv->r_irq) { + dev_err(&pdev->dev, "Unable to get register irq resource.\n"); + return -ENODEV; + } + + ret = devm_request_irq(&pdev->dev, i2s_priv->r_irq->start, i2s_dma_isr, + i2s_priv->r_irq->flags, "i2s_dma", (void *)i2s_priv); + if (ret) { + dev_err(&pdev->dev, + "i2s_init: failed to request interrupt.ret=%d\n", ret); + return ret; + } + + return devm_snd_soc_register_component(&pdev->dev, + &bcm63xx_soc_platform, NULL, 0); +} + +int bcm63xx_soc_platform_remove(struct platform_device *pdev) +{ + return 0; +} + +MODULE_AUTHOR("Kevin,Li <kevin-ke.li@broadcom.com>"); +MODULE_DESCRIPTION("Broadcom DSL XPON ASOC PCM Interface"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/bcm/cygnus-pcm.c b/sound/soc/bcm/cygnus-pcm.c new file mode 100644 index 000000000..7ad07239f --- /dev/null +++ b/sound/soc/bcm/cygnus-pcm.c @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2014-2015 Broadcom Corporation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation version 2. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include <linux/debugfs.h> +#include <linux/dma-mapping.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dai.h> + +#include "cygnus-ssp.h" + +/* Register offset needed for ASoC PCM module */ + +#define INTH_R5F_STATUS_OFFSET 0x040 +#define INTH_R5F_CLEAR_OFFSET 0x048 +#define INTH_R5F_MASK_SET_OFFSET 0x050 +#define INTH_R5F_MASK_CLEAR_OFFSET 0x054 + +#define BF_REARM_FREE_MARK_OFFSET 0x344 +#define BF_REARM_FULL_MARK_OFFSET 0x348 + +/* Ring Buffer Ctrl Regs --- Start */ +/* AUD_FMM_BF_CTRL_SOURCECH_RINGBUF_X_RDADDR_REG_BASE */ +#define SRC_RBUF_0_RDADDR_OFFSET 0x500 +#define SRC_RBUF_1_RDADDR_OFFSET 0x518 +#define SRC_RBUF_2_RDADDR_OFFSET 0x530 +#define SRC_RBUF_3_RDADDR_OFFSET 0x548 +#define SRC_RBUF_4_RDADDR_OFFSET 0x560 +#define SRC_RBUF_5_RDADDR_OFFSET 0x578 +#define SRC_RBUF_6_RDADDR_OFFSET 0x590 + +/* AUD_FMM_BF_CTRL_SOURCECH_RINGBUF_X_WRADDR_REG_BASE */ +#define SRC_RBUF_0_WRADDR_OFFSET 0x504 +#define SRC_RBUF_1_WRADDR_OFFSET 0x51c +#define SRC_RBUF_2_WRADDR_OFFSET 0x534 +#define SRC_RBUF_3_WRADDR_OFFSET 0x54c +#define SRC_RBUF_4_WRADDR_OFFSET 0x564 +#define SRC_RBUF_5_WRADDR_OFFSET 0x57c +#define SRC_RBUF_6_WRADDR_OFFSET 0x594 + +/* AUD_FMM_BF_CTRL_SOURCECH_RINGBUF_X_BASEADDR_REG_BASE */ +#define SRC_RBUF_0_BASEADDR_OFFSET 0x508 +#define SRC_RBUF_1_BASEADDR_OFFSET 0x520 +#define SRC_RBUF_2_BASEADDR_OFFSET 0x538 +#define SRC_RBUF_3_BASEADDR_OFFSET 0x550 +#define SRC_RBUF_4_BASEADDR_OFFSET 0x568 +#define SRC_RBUF_5_BASEADDR_OFFSET 0x580 +#define SRC_RBUF_6_BASEADDR_OFFSET 0x598 + +/* AUD_FMM_BF_CTRL_SOURCECH_RINGBUF_X_ENDADDR_REG_BASE */ +#define SRC_RBUF_0_ENDADDR_OFFSET 0x50c +#define SRC_RBUF_1_ENDADDR_OFFSET 0x524 +#define SRC_RBUF_2_ENDADDR_OFFSET 0x53c +#define SRC_RBUF_3_ENDADDR_OFFSET 0x554 +#define SRC_RBUF_4_ENDADDR_OFFSET 0x56c +#define SRC_RBUF_5_ENDADDR_OFFSET 0x584 +#define SRC_RBUF_6_ENDADDR_OFFSET 0x59c + +/* AUD_FMM_BF_CTRL_SOURCECH_RINGBUF_X_FREE_MARK_REG_BASE */ +#define SRC_RBUF_0_FREE_MARK_OFFSET 0x510 +#define SRC_RBUF_1_FREE_MARK_OFFSET 0x528 +#define SRC_RBUF_2_FREE_MARK_OFFSET 0x540 +#define SRC_RBUF_3_FREE_MARK_OFFSET 0x558 +#define SRC_RBUF_4_FREE_MARK_OFFSET 0x570 +#define SRC_RBUF_5_FREE_MARK_OFFSET 0x588 +#define SRC_RBUF_6_FREE_MARK_OFFSET 0x5a0 + +/* AUD_FMM_BF_CTRL_DESTCH_RINGBUF_X_RDADDR_REG_BASE */ +#define DST_RBUF_0_RDADDR_OFFSET 0x5c0 +#define DST_RBUF_1_RDADDR_OFFSET 0x5d8 +#define DST_RBUF_2_RDADDR_OFFSET 0x5f0 +#define DST_RBUF_3_RDADDR_OFFSET 0x608 +#define DST_RBUF_4_RDADDR_OFFSET 0x620 +#define DST_RBUF_5_RDADDR_OFFSET 0x638 + +/* AUD_FMM_BF_CTRL_DESTCH_RINGBUF_X_WRADDR_REG_BASE */ +#define DST_RBUF_0_WRADDR_OFFSET 0x5c4 +#define DST_RBUF_1_WRADDR_OFFSET 0x5dc +#define DST_RBUF_2_WRADDR_OFFSET 0x5f4 +#define DST_RBUF_3_WRADDR_OFFSET 0x60c +#define DST_RBUF_4_WRADDR_OFFSET 0x624 +#define DST_RBUF_5_WRADDR_OFFSET 0x63c + +/* AUD_FMM_BF_CTRL_DESTCH_RINGBUF_X_BASEADDR_REG_BASE */ +#define DST_RBUF_0_BASEADDR_OFFSET 0x5c8 +#define DST_RBUF_1_BASEADDR_OFFSET 0x5e0 +#define DST_RBUF_2_BASEADDR_OFFSET 0x5f8 +#define DST_RBUF_3_BASEADDR_OFFSET 0x610 +#define DST_RBUF_4_BASEADDR_OFFSET 0x628 +#define DST_RBUF_5_BASEADDR_OFFSET 0x640 + +/* AUD_FMM_BF_CTRL_DESTCH_RINGBUF_X_ENDADDR_REG_BASE */ +#define DST_RBUF_0_ENDADDR_OFFSET 0x5cc +#define DST_RBUF_1_ENDADDR_OFFSET 0x5e4 +#define DST_RBUF_2_ENDADDR_OFFSET 0x5fc +#define DST_RBUF_3_ENDADDR_OFFSET 0x614 +#define DST_RBUF_4_ENDADDR_OFFSET 0x62c +#define DST_RBUF_5_ENDADDR_OFFSET 0x644 + +/* AUD_FMM_BF_CTRL_DESTCH_RINGBUF_X_FULL_MARK_REG_BASE */ +#define DST_RBUF_0_FULL_MARK_OFFSET 0x5d0 +#define DST_RBUF_1_FULL_MARK_OFFSET 0x5e8 +#define DST_RBUF_2_FULL_MARK_OFFSET 0x600 +#define DST_RBUF_3_FULL_MARK_OFFSET 0x618 +#define DST_RBUF_4_FULL_MARK_OFFSET 0x630 +#define DST_RBUF_5_FULL_MARK_OFFSET 0x648 +/* Ring Buffer Ctrl Regs --- End */ + +/* Error Status Regs --- Start */ +/* AUD_FMM_BF_ESR_ESRX_STATUS_REG_BASE */ +#define ESR0_STATUS_OFFSET 0x900 +#define ESR1_STATUS_OFFSET 0x918 +#define ESR2_STATUS_OFFSET 0x930 +#define ESR3_STATUS_OFFSET 0x948 +#define ESR4_STATUS_OFFSET 0x960 + +/* AUD_FMM_BF_ESR_ESRX_STATUS_CLEAR_REG_BASE */ +#define ESR0_STATUS_CLR_OFFSET 0x908 +#define ESR1_STATUS_CLR_OFFSET 0x920 +#define ESR2_STATUS_CLR_OFFSET 0x938 +#define ESR3_STATUS_CLR_OFFSET 0x950 +#define ESR4_STATUS_CLR_OFFSET 0x968 + +/* AUD_FMM_BF_ESR_ESRX_MASK_REG_BASE */ +#define ESR0_MASK_STATUS_OFFSET 0x90c +#define ESR1_MASK_STATUS_OFFSET 0x924 +#define ESR2_MASK_STATUS_OFFSET 0x93c +#define ESR3_MASK_STATUS_OFFSET 0x954 +#define ESR4_MASK_STATUS_OFFSET 0x96c + +/* AUD_FMM_BF_ESR_ESRX_MASK_SET_REG_BASE */ +#define ESR0_MASK_SET_OFFSET 0x910 +#define ESR1_MASK_SET_OFFSET 0x928 +#define ESR2_MASK_SET_OFFSET 0x940 +#define ESR3_MASK_SET_OFFSET 0x958 +#define ESR4_MASK_SET_OFFSET 0x970 + +/* AUD_FMM_BF_ESR_ESRX_MASK_CLEAR_REG_BASE */ +#define ESR0_MASK_CLR_OFFSET 0x914 +#define ESR1_MASK_CLR_OFFSET 0x92c +#define ESR2_MASK_CLR_OFFSET 0x944 +#define ESR3_MASK_CLR_OFFSET 0x95c +#define ESR4_MASK_CLR_OFFSET 0x974 +/* Error Status Regs --- End */ + +#define R5F_ESR0_SHIFT 0 /* esr0 = fifo underflow */ +#define R5F_ESR1_SHIFT 1 /* esr1 = ringbuf underflow */ +#define R5F_ESR2_SHIFT 2 /* esr2 = ringbuf overflow */ +#define R5F_ESR3_SHIFT 3 /* esr3 = freemark */ +#define R5F_ESR4_SHIFT 4 /* esr4 = fullmark */ + + +/* Mask for R5F register. Set all relevant interrupt for playback handler */ +#define ANY_PLAYBACK_IRQ (BIT(R5F_ESR0_SHIFT) | \ + BIT(R5F_ESR1_SHIFT) | \ + BIT(R5F_ESR3_SHIFT)) + +/* Mask for R5F register. Set all relevant interrupt for capture handler */ +#define ANY_CAPTURE_IRQ (BIT(R5F_ESR2_SHIFT) | BIT(R5F_ESR4_SHIFT)) + +/* + * PERIOD_BYTES_MIN is the number of bytes to at which the interrupt will tick. + * This number should be a multiple of 256. Minimum value is 256 + */ +#define PERIOD_BYTES_MIN 0x100 + +static const struct snd_pcm_hardware cygnus_pcm_hw = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED, + .formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S32_LE, + + /* A period is basically an interrupt */ + .period_bytes_min = PERIOD_BYTES_MIN, + .period_bytes_max = 0x10000, + + /* period_min/max gives range of approx interrupts per buffer */ + .periods_min = 2, + .periods_max = 8, + + /* + * maximum buffer size in bytes = period_bytes_max * periods_max + * We allocate this amount of data for each enabled channel + */ + .buffer_bytes_max = 4 * 0x8000, +}; + +static u64 cygnus_dma_dmamask = DMA_BIT_MASK(32); + +static struct cygnus_aio_port *cygnus_dai_get_dma_data( + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *soc_runtime = asoc_substream_to_rtd(substream); + + return snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(soc_runtime, 0), substream); +} + +static void ringbuf_set_initial(void __iomem *audio_io, + struct ringbuf_regs *p_rbuf, + bool is_playback, + u32 start, + u32 periodsize, + u32 bufsize) +{ + u32 initial_rd; + u32 initial_wr; + u32 end; + u32 fmark_val; /* free or full mark */ + + p_rbuf->period_bytes = periodsize; + p_rbuf->buf_size = bufsize; + + if (is_playback) { + /* Set the pointers to indicate full (flip uppermost bit) */ + initial_rd = start; + initial_wr = initial_rd ^ BIT(31); + } else { + /* Set the pointers to indicate empty */ + initial_wr = start; + initial_rd = initial_wr; + } + + end = start + bufsize - 1; + + /* + * The interrupt will fire when free/full mark is *exceeded* + * The fmark value must be multiple of PERIOD_BYTES_MIN so set fmark + * to be PERIOD_BYTES_MIN less than the period size. + */ + fmark_val = periodsize - PERIOD_BYTES_MIN; + + writel(start, audio_io + p_rbuf->baseaddr); + writel(end, audio_io + p_rbuf->endaddr); + writel(fmark_val, audio_io + p_rbuf->fmark); + writel(initial_rd, audio_io + p_rbuf->rdaddr); + writel(initial_wr, audio_io + p_rbuf->wraddr); +} + +static int configure_ringbuf_regs(struct snd_pcm_substream *substream) +{ + struct cygnus_aio_port *aio; + struct ringbuf_regs *p_rbuf; + int status = 0; + + aio = cygnus_dai_get_dma_data(substream); + + /* Map the ssp portnum to a set of ring buffers. */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + p_rbuf = &aio->play_rb_regs; + + switch (aio->portnum) { + case 0: + *p_rbuf = RINGBUF_REG_PLAYBACK(0); + break; + case 1: + *p_rbuf = RINGBUF_REG_PLAYBACK(2); + break; + case 2: + *p_rbuf = RINGBUF_REG_PLAYBACK(4); + break; + case 3: /* SPDIF */ + *p_rbuf = RINGBUF_REG_PLAYBACK(6); + break; + default: + status = -EINVAL; + } + } else { + p_rbuf = &aio->capture_rb_regs; + + switch (aio->portnum) { + case 0: + *p_rbuf = RINGBUF_REG_CAPTURE(0); + break; + case 1: + *p_rbuf = RINGBUF_REG_CAPTURE(2); + break; + case 2: + *p_rbuf = RINGBUF_REG_CAPTURE(4); + break; + default: + status = -EINVAL; + } + } + + return status; +} + +static struct ringbuf_regs *get_ringbuf(struct snd_pcm_substream *substream) +{ + struct cygnus_aio_port *aio; + struct ringbuf_regs *p_rbuf = NULL; + + aio = cygnus_dai_get_dma_data(substream); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + p_rbuf = &aio->play_rb_regs; + else + p_rbuf = &aio->capture_rb_regs; + + return p_rbuf; +} + +static void enable_intr(struct snd_pcm_substream *substream) +{ + struct cygnus_aio_port *aio; + u32 clear_mask; + + aio = cygnus_dai_get_dma_data(substream); + + /* The port number maps to the bit position to be cleared */ + clear_mask = BIT(aio->portnum); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* Clear interrupt status before enabling them */ + writel(clear_mask, aio->cygaud->audio + ESR0_STATUS_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR1_STATUS_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR3_STATUS_CLR_OFFSET); + /* Unmask the interrupts of the given port*/ + writel(clear_mask, aio->cygaud->audio + ESR0_MASK_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR1_MASK_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR3_MASK_CLR_OFFSET); + + writel(ANY_PLAYBACK_IRQ, + aio->cygaud->audio + INTH_R5F_MASK_CLEAR_OFFSET); + } else { + writel(clear_mask, aio->cygaud->audio + ESR2_STATUS_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR4_STATUS_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR2_MASK_CLR_OFFSET); + writel(clear_mask, aio->cygaud->audio + ESR4_MASK_CLR_OFFSET); + + writel(ANY_CAPTURE_IRQ, + aio->cygaud->audio + INTH_R5F_MASK_CLEAR_OFFSET); + } + +} + +static void disable_intr(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct cygnus_aio_port *aio; + u32 set_mask; + + aio = cygnus_dai_get_dma_data(substream); + + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s on port %d\n", __func__, aio->portnum); + + /* The port number maps to the bit position to be set */ + set_mask = BIT(aio->portnum); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* Mask the interrupts of the given port*/ + writel(set_mask, aio->cygaud->audio + ESR0_MASK_SET_OFFSET); + writel(set_mask, aio->cygaud->audio + ESR1_MASK_SET_OFFSET); + writel(set_mask, aio->cygaud->audio + ESR3_MASK_SET_OFFSET); + } else { + writel(set_mask, aio->cygaud->audio + ESR2_MASK_SET_OFFSET); + writel(set_mask, aio->cygaud->audio + ESR4_MASK_SET_OFFSET); + } + +} + +static int cygnus_pcm_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + int ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + enable_intr(substream); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + disable_intr(substream); + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static void cygnus_pcm_period_elapsed(struct snd_pcm_substream *substream) +{ + struct cygnus_aio_port *aio; + struct ringbuf_regs *p_rbuf = NULL; + u32 regval; + + aio = cygnus_dai_get_dma_data(substream); + + p_rbuf = get_ringbuf(substream); + + /* + * If free/full mark interrupt occurs, provide timestamp + * to ALSA and update appropriate idx by period_bytes + */ + snd_pcm_period_elapsed(substream); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* Set the ring buffer to full */ + regval = readl(aio->cygaud->audio + p_rbuf->rdaddr); + regval = regval ^ BIT(31); + writel(regval, aio->cygaud->audio + p_rbuf->wraddr); + } else { + /* Set the ring buffer to empty */ + regval = readl(aio->cygaud->audio + p_rbuf->wraddr); + writel(regval, aio->cygaud->audio + p_rbuf->rdaddr); + } +} + +/* + * ESR0/1/3 status Description + * 0x1 I2S0_out port caused interrupt + * 0x2 I2S1_out port caused interrupt + * 0x4 I2S2_out port caused interrupt + * 0x8 SPDIF_out port caused interrupt + */ +static void handle_playback_irq(struct cygnus_audio *cygaud) +{ + void __iomem *audio_io; + u32 port; + u32 esr_status0, esr_status1, esr_status3; + + audio_io = cygaud->audio; + + /* + * ESR status gets updates with/without interrupts enabled. + * So, check the ESR mask, which provides interrupt enable/ + * disable status and use it to determine which ESR status + * should be serviced. + */ + esr_status0 = readl(audio_io + ESR0_STATUS_OFFSET); + esr_status0 &= ~readl(audio_io + ESR0_MASK_STATUS_OFFSET); + esr_status1 = readl(audio_io + ESR1_STATUS_OFFSET); + esr_status1 &= ~readl(audio_io + ESR1_MASK_STATUS_OFFSET); + esr_status3 = readl(audio_io + ESR3_STATUS_OFFSET); + esr_status3 &= ~readl(audio_io + ESR3_MASK_STATUS_OFFSET); + + for (port = 0; port < CYGNUS_MAX_PLAYBACK_PORTS; port++) { + u32 esrmask = BIT(port); + + /* + * Ringbuffer or FIFO underflow + * If we get this interrupt then, it is also true that we have + * not yet responded to the freemark interrupt. + * Log a debug message. The freemark handler below will + * handle getting everything going again. + */ + if ((esrmask & esr_status1) || (esrmask & esr_status0)) { + dev_dbg(cygaud->dev, + "Underrun: esr0=0x%x, esr1=0x%x esr3=0x%x\n", + esr_status0, esr_status1, esr_status3); + } + + /* + * Freemark is hit. This is the normal interrupt. + * In typical operation the read and write regs will be equal + */ + if (esrmask & esr_status3) { + struct snd_pcm_substream *playstr; + + playstr = cygaud->portinfo[port].play_stream; + cygnus_pcm_period_elapsed(playstr); + } + } + + /* Clear ESR interrupt */ + writel(esr_status0, audio_io + ESR0_STATUS_CLR_OFFSET); + writel(esr_status1, audio_io + ESR1_STATUS_CLR_OFFSET); + writel(esr_status3, audio_io + ESR3_STATUS_CLR_OFFSET); + /* Rearm freemark logic by writing 1 to the correct bit */ + writel(esr_status3, audio_io + BF_REARM_FREE_MARK_OFFSET); +} + +/* + * ESR2/4 status Description + * 0x1 I2S0_in port caused interrupt + * 0x2 I2S1_in port caused interrupt + * 0x4 I2S2_in port caused interrupt + */ +static void handle_capture_irq(struct cygnus_audio *cygaud) +{ + void __iomem *audio_io; + u32 port; + u32 esr_status2, esr_status4; + + audio_io = cygaud->audio; + + /* + * ESR status gets updates with/without interrupts enabled. + * So, check the ESR mask, which provides interrupt enable/ + * disable status and use it to determine which ESR status + * should be serviced. + */ + esr_status2 = readl(audio_io + ESR2_STATUS_OFFSET); + esr_status2 &= ~readl(audio_io + ESR2_MASK_STATUS_OFFSET); + esr_status4 = readl(audio_io + ESR4_STATUS_OFFSET); + esr_status4 &= ~readl(audio_io + ESR4_MASK_STATUS_OFFSET); + + for (port = 0; port < CYGNUS_MAX_CAPTURE_PORTS; port++) { + u32 esrmask = BIT(port); + + /* + * Ringbuffer or FIFO overflow + * If we get this interrupt then, it is also true that we have + * not yet responded to the fullmark interrupt. + * Log a debug message. The fullmark handler below will + * handle getting everything going again. + */ + if (esrmask & esr_status2) + dev_dbg(cygaud->dev, + "Overflow: esr2=0x%x\n", esr_status2); + + if (esrmask & esr_status4) { + struct snd_pcm_substream *capstr; + + capstr = cygaud->portinfo[port].capture_stream; + cygnus_pcm_period_elapsed(capstr); + } + } + + writel(esr_status2, audio_io + ESR2_STATUS_CLR_OFFSET); + writel(esr_status4, audio_io + ESR4_STATUS_CLR_OFFSET); + /* Rearm fullmark logic by writing 1 to the correct bit */ + writel(esr_status4, audio_io + BF_REARM_FULL_MARK_OFFSET); +} + +static irqreturn_t cygnus_dma_irq(int irq, void *data) +{ + u32 r5_status; + struct cygnus_audio *cygaud = data; + + /* + * R5 status bits Description + * 0 ESR0 (playback FIFO interrupt) + * 1 ESR1 (playback rbuf interrupt) + * 2 ESR2 (capture rbuf interrupt) + * 3 ESR3 (Freemark play. interrupt) + * 4 ESR4 (Fullmark capt. interrupt) + */ + r5_status = readl(cygaud->audio + INTH_R5F_STATUS_OFFSET); + + if (!(r5_status & (ANY_PLAYBACK_IRQ | ANY_CAPTURE_IRQ))) + return IRQ_NONE; + + /* If playback interrupt happened */ + if (ANY_PLAYBACK_IRQ & r5_status) { + handle_playback_irq(cygaud); + writel(ANY_PLAYBACK_IRQ & r5_status, + cygaud->audio + INTH_R5F_CLEAR_OFFSET); + } + + /* If capture interrupt happened */ + if (ANY_CAPTURE_IRQ & r5_status) { + handle_capture_irq(cygaud); + writel(ANY_CAPTURE_IRQ & r5_status, + cygaud->audio + INTH_R5F_CLEAR_OFFSET); + } + + return IRQ_HANDLED; +} + +static int cygnus_pcm_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + struct cygnus_aio_port *aio; + int ret; + + aio = cygnus_dai_get_dma_data(substream); + if (!aio) + return -ENODEV; + + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s port %d\n", __func__, aio->portnum); + + snd_soc_set_runtime_hwparams(substream, &cygnus_pcm_hw); + + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_PERIOD_BYTES, PERIOD_BYTES_MIN); + if (ret < 0) + return ret; + + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_BUFFER_BYTES, PERIOD_BYTES_MIN); + if (ret < 0) + return ret; + /* + * Keep track of which substream belongs to which port. + * This info is needed by snd_pcm_period_elapsed() in irq_handler + */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + aio->play_stream = substream; + else + aio->capture_stream = substream; + + return 0; +} + +static int cygnus_pcm_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct cygnus_aio_port *aio; + + aio = cygnus_dai_get_dma_data(substream); + + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s port %d\n", __func__, aio->portnum); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + aio->play_stream = NULL; + else + aio->capture_stream = NULL; + + if (!aio->play_stream && !aio->capture_stream) + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "freed port %d\n", aio->portnum); + + return 0; +} + +static int cygnus_pcm_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + struct cygnus_aio_port *aio; + + aio = cygnus_dai_get_dma_data(substream); + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s port %d\n", __func__, aio->portnum); + + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); + runtime->dma_bytes = params_buffer_bytes(params); + + return 0; +} + +static int cygnus_pcm_hw_free(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct cygnus_aio_port *aio; + + aio = cygnus_dai_get_dma_data(substream); + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s port %d\n", __func__, aio->portnum); + + snd_pcm_set_runtime_buffer(substream, NULL); + return 0; +} + +static int cygnus_pcm_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + struct cygnus_aio_port *aio; + unsigned long bufsize, periodsize; + bool is_play; + u32 start; + struct ringbuf_regs *p_rbuf = NULL; + + aio = cygnus_dai_get_dma_data(substream); + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s port %d\n", __func__, aio->portnum); + + bufsize = snd_pcm_lib_buffer_bytes(substream); + periodsize = snd_pcm_lib_period_bytes(substream); + + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s (buf_size %lu) (period_size %lu)\n", + __func__, bufsize, periodsize); + + configure_ringbuf_regs(substream); + + p_rbuf = get_ringbuf(substream); + + start = runtime->dma_addr; + + is_play = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? 1 : 0; + + ringbuf_set_initial(aio->cygaud->audio, p_rbuf, is_play, start, + periodsize, bufsize); + + return 0; +} + +static snd_pcm_uframes_t cygnus_pcm_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct cygnus_aio_port *aio; + unsigned int res = 0, cur = 0, base = 0; + struct ringbuf_regs *p_rbuf = NULL; + + aio = cygnus_dai_get_dma_data(substream); + + /* + * Get the offset of the current read (for playack) or write + * index (for capture). Report this value back to the asoc framework. + */ + p_rbuf = get_ringbuf(substream); + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cur = readl(aio->cygaud->audio + p_rbuf->rdaddr); + else + cur = readl(aio->cygaud->audio + p_rbuf->wraddr); + + base = readl(aio->cygaud->audio + p_rbuf->baseaddr); + + /* + * Mask off the MSB of the rdaddr,wraddr and baseaddr + * since MSB is not part of the address + */ + res = (cur & 0x7fffffff) - (base & 0x7fffffff); + + return bytes_to_frames(substream->runtime, res); +} + +static int cygnus_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, int stream) +{ + struct snd_pcm_substream *substream = pcm->streams[stream].substream; + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_dma_buffer *buf = &substream->dma_buffer; + size_t size; + + size = cygnus_pcm_hw.buffer_bytes_max; + + buf->dev.type = SNDRV_DMA_TYPE_DEV; + buf->dev.dev = pcm->card->dev; + buf->private_data = NULL; + buf->area = dma_alloc_coherent(pcm->card->dev, size, + &buf->addr, GFP_KERNEL); + + dev_dbg(asoc_rtd_to_cpu(rtd, 0)->dev, "%s: size 0x%zx @ %pK\n", + __func__, size, buf->area); + + if (!buf->area) { + dev_err(asoc_rtd_to_cpu(rtd, 0)->dev, "%s: dma_alloc failed\n", __func__); + return -ENOMEM; + } + buf->bytes = size; + + return 0; +} + +static void cygnus_dma_free_dma_buffers(struct snd_soc_component *component, + struct snd_pcm *pcm) +{ + struct snd_pcm_substream *substream; + struct snd_dma_buffer *buf; + + substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; + if (substream) { + buf = &substream->dma_buffer; + if (buf->area) { + dma_free_coherent(pcm->card->dev, buf->bytes, + buf->area, buf->addr); + buf->area = NULL; + } + } + + substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; + if (substream) { + buf = &substream->dma_buffer; + if (buf->area) { + dma_free_coherent(pcm->card->dev, buf->bytes, + buf->area, buf->addr); + buf->area = NULL; + } + } +} + +static int cygnus_dma_new(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct snd_card *card = rtd->card->snd_card; + struct snd_pcm *pcm = rtd->pcm; + int ret; + + if (!card->dev->dma_mask) + card->dev->dma_mask = &cygnus_dma_dmamask; + if (!card->dev->coherent_dma_mask) + card->dev->coherent_dma_mask = DMA_BIT_MASK(32); + + if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) { + ret = cygnus_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_PLAYBACK); + if (ret) + return ret; + } + + if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) { + ret = cygnus_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_CAPTURE); + if (ret) { + cygnus_dma_free_dma_buffers(component, pcm); + return ret; + } + } + + return 0; +} + +static struct snd_soc_component_driver cygnus_soc_platform = { + .open = cygnus_pcm_open, + .close = cygnus_pcm_close, + .hw_params = cygnus_pcm_hw_params, + .hw_free = cygnus_pcm_hw_free, + .prepare = cygnus_pcm_prepare, + .trigger = cygnus_pcm_trigger, + .pointer = cygnus_pcm_pointer, + .pcm_construct = cygnus_dma_new, + .pcm_destruct = cygnus_dma_free_dma_buffers, +}; + +int cygnus_soc_platform_register(struct device *dev, + struct cygnus_audio *cygaud) +{ + int rc = 0; + + dev_dbg(dev, "%s Enter\n", __func__); + + rc = devm_request_irq(dev, cygaud->irq_num, cygnus_dma_irq, + IRQF_SHARED, "cygnus-audio", cygaud); + if (rc) { + dev_err(dev, "%s request_irq error %d\n", __func__, rc); + return rc; + } + + rc = devm_snd_soc_register_component(dev, &cygnus_soc_platform, + NULL, 0); + if (rc) { + dev_err(dev, "%s failed\n", __func__); + return rc; + } + + return 0; +} + +int cygnus_soc_platform_unregister(struct device *dev) +{ + return 0; +} + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Broadcom"); +MODULE_DESCRIPTION("Cygnus ASoC PCM module"); diff --git a/sound/soc/bcm/cygnus-ssp.c b/sound/soc/bcm/cygnus-ssp.c new file mode 100644 index 000000000..6e634b448 --- /dev/null +++ b/sound/soc/bcm/cygnus-ssp.c @@ -0,0 +1,1417 @@ +/* + * Copyright (C) 2014-2015 Broadcom Corporation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation version 2. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/slab.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dai.h> + +#include "cygnus-ssp.h" + +#define DEFAULT_VCO 1354750204 + +#define CAPTURE_FCI_ID_BASE 0x180 +#define CYGNUS_SSP_TRISTATE_MASK 0x001fff +#define CYGNUS_PLLCLKSEL_MASK 0xf + +/* Used with stream_on field to indicate which streams are active */ +#define PLAYBACK_STREAM_MASK BIT(0) +#define CAPTURE_STREAM_MASK BIT(1) + +#define I2S_STREAM_CFG_MASK 0xff003ff +#define I2S_CAP_STREAM_CFG_MASK 0xf0 +#define SPDIF_STREAM_CFG_MASK 0x3ff +#define CH_GRP_STEREO 0x1 + +/* Begin register offset defines */ +#define AUD_MISC_SEROUT_OE_REG_BASE 0x01c +#define AUD_MISC_SEROUT_SPDIF_OE 12 +#define AUD_MISC_SEROUT_MCLK_OE 3 +#define AUD_MISC_SEROUT_LRCK_OE 2 +#define AUD_MISC_SEROUT_SCLK_OE 1 +#define AUD_MISC_SEROUT_SDAT_OE 0 + +/* AUD_FMM_BF_CTRL_xxx regs */ +#define BF_DST_CFG0_OFFSET 0x100 +#define BF_DST_CFG1_OFFSET 0x104 +#define BF_DST_CFG2_OFFSET 0x108 + +#define BF_DST_CTRL0_OFFSET 0x130 +#define BF_DST_CTRL1_OFFSET 0x134 +#define BF_DST_CTRL2_OFFSET 0x138 + +#define BF_SRC_CFG0_OFFSET 0x148 +#define BF_SRC_CFG1_OFFSET 0x14c +#define BF_SRC_CFG2_OFFSET 0x150 +#define BF_SRC_CFG3_OFFSET 0x154 + +#define BF_SRC_CTRL0_OFFSET 0x1c0 +#define BF_SRC_CTRL1_OFFSET 0x1c4 +#define BF_SRC_CTRL2_OFFSET 0x1c8 +#define BF_SRC_CTRL3_OFFSET 0x1cc + +#define BF_SRC_GRP0_OFFSET 0x1fc +#define BF_SRC_GRP1_OFFSET 0x200 +#define BF_SRC_GRP2_OFFSET 0x204 +#define BF_SRC_GRP3_OFFSET 0x208 + +#define BF_SRC_GRP_EN_OFFSET 0x320 +#define BF_SRC_GRP_FLOWON_OFFSET 0x324 +#define BF_SRC_GRP_SYNC_DIS_OFFSET 0x328 + +/* AUD_FMM_IOP_OUT_I2S_xxx regs */ +#define OUT_I2S_0_STREAM_CFG_OFFSET 0xa00 +#define OUT_I2S_0_CFG_OFFSET 0xa04 +#define OUT_I2S_0_MCLK_CFG_OFFSET 0xa0c + +#define OUT_I2S_1_STREAM_CFG_OFFSET 0xa40 +#define OUT_I2S_1_CFG_OFFSET 0xa44 +#define OUT_I2S_1_MCLK_CFG_OFFSET 0xa4c + +#define OUT_I2S_2_STREAM_CFG_OFFSET 0xa80 +#define OUT_I2S_2_CFG_OFFSET 0xa84 +#define OUT_I2S_2_MCLK_CFG_OFFSET 0xa8c + +/* AUD_FMM_IOP_OUT_SPDIF_xxx regs */ +#define SPDIF_STREAM_CFG_OFFSET 0xac0 +#define SPDIF_CTRL_OFFSET 0xac4 +#define SPDIF_FORMAT_CFG_OFFSET 0xad8 +#define SPDIF_MCLK_CFG_OFFSET 0xadc + +/* AUD_FMM_IOP_PLL_0_xxx regs */ +#define IOP_PLL_0_MACRO_OFFSET 0xb00 +#define IOP_PLL_0_MDIV_Ch0_OFFSET 0xb14 +#define IOP_PLL_0_MDIV_Ch1_OFFSET 0xb18 +#define IOP_PLL_0_MDIV_Ch2_OFFSET 0xb1c + +#define IOP_PLL_0_ACTIVE_MDIV_Ch0_OFFSET 0xb30 +#define IOP_PLL_0_ACTIVE_MDIV_Ch1_OFFSET 0xb34 +#define IOP_PLL_0_ACTIVE_MDIV_Ch2_OFFSET 0xb38 + +/* AUD_FMM_IOP_xxx regs */ +#define IOP_PLL_0_CONTROL_OFFSET 0xb04 +#define IOP_PLL_0_USER_NDIV_OFFSET 0xb08 +#define IOP_PLL_0_ACTIVE_NDIV_OFFSET 0xb20 +#define IOP_PLL_0_RESET_OFFSET 0xb5c + +/* AUD_FMM_IOP_IN_I2S_xxx regs */ +#define IN_I2S_0_STREAM_CFG_OFFSET 0x00 +#define IN_I2S_0_CFG_OFFSET 0x04 +#define IN_I2S_1_STREAM_CFG_OFFSET 0x40 +#define IN_I2S_1_CFG_OFFSET 0x44 +#define IN_I2S_2_STREAM_CFG_OFFSET 0x80 +#define IN_I2S_2_CFG_OFFSET 0x84 + +/* AUD_FMM_IOP_MISC_xxx regs */ +#define IOP_SW_INIT_LOGIC 0x1c0 + +/* End register offset defines */ + + +/* AUD_FMM_IOP_OUT_I2S_x_MCLK_CFG_0_REG */ +#define I2S_OUT_MCLKRATE_SHIFT 16 + +/* AUD_FMM_IOP_OUT_I2S_x_MCLK_CFG_REG */ +#define I2S_OUT_PLLCLKSEL_SHIFT 0 + +/* AUD_FMM_IOP_OUT_I2S_x_STREAM_CFG */ +#define I2S_OUT_STREAM_ENA 31 +#define I2S_OUT_STREAM_CFG_GROUP_ID 20 +#define I2S_OUT_STREAM_CFG_CHANNEL_GROUPING 24 + +/* AUD_FMM_IOP_IN_I2S_x_CAP */ +#define I2S_IN_STREAM_CFG_CAP_ENA 31 +#define I2S_IN_STREAM_CFG_0_GROUP_ID 4 + +/* AUD_FMM_IOP_OUT_I2S_x_I2S_CFG_REG */ +#define I2S_OUT_CFGX_CLK_ENA 0 +#define I2S_OUT_CFGX_DATA_ENABLE 1 +#define I2S_OUT_CFGX_DATA_ALIGNMENT 6 +#define I2S_OUT_CFGX_BITS_PER_SLOT 13 +#define I2S_OUT_CFGX_VALID_SLOT 14 +#define I2S_OUT_CFGX_FSYNC_WIDTH 18 +#define I2S_OUT_CFGX_SCLKS_PER_1FS_DIV32 26 +#define I2S_OUT_CFGX_SLAVE_MODE 30 +#define I2S_OUT_CFGX_TDM_MODE 31 + +/* AUD_FMM_BF_CTRL_SOURCECH_CFGx_REG */ +#define BF_SRC_CFGX_SFIFO_ENA 0 +#define BF_SRC_CFGX_BUFFER_PAIR_ENABLE 1 +#define BF_SRC_CFGX_SAMPLE_CH_MODE 2 +#define BF_SRC_CFGX_SFIFO_SZ_DOUBLE 5 +#define BF_SRC_CFGX_NOT_PAUSE_WHEN_EMPTY 10 +#define BF_SRC_CFGX_BIT_RES 20 +#define BF_SRC_CFGX_PROCESS_SEQ_ID_VALID 31 + +/* AUD_FMM_BF_CTRL_DESTCH_CFGx_REG */ +#define BF_DST_CFGX_CAP_ENA 0 +#define BF_DST_CFGX_BUFFER_PAIR_ENABLE 1 +#define BF_DST_CFGX_DFIFO_SZ_DOUBLE 2 +#define BF_DST_CFGX_NOT_PAUSE_WHEN_FULL 11 +#define BF_DST_CFGX_FCI_ID 12 +#define BF_DST_CFGX_CAP_MODE 24 +#define BF_DST_CFGX_PROC_SEQ_ID_VALID 31 + +/* AUD_FMM_IOP_OUT_SPDIF_xxx */ +#define SPDIF_0_OUT_DITHER_ENA 3 +#define SPDIF_0_OUT_STREAM_ENA 31 + +/* AUD_FMM_IOP_PLL_0_USER */ +#define IOP_PLL_0_USER_NDIV_FRAC 10 + +/* AUD_FMM_IOP_PLL_0_ACTIVE */ +#define IOP_PLL_0_ACTIVE_NDIV_FRAC 10 + + +#define INIT_SSP_REGS(num) (struct cygnus_ssp_regs){ \ + .i2s_stream_cfg = OUT_I2S_ ##num## _STREAM_CFG_OFFSET, \ + .i2s_cap_stream_cfg = IN_I2S_ ##num## _STREAM_CFG_OFFSET, \ + .i2s_cfg = OUT_I2S_ ##num## _CFG_OFFSET, \ + .i2s_cap_cfg = IN_I2S_ ##num## _CFG_OFFSET, \ + .i2s_mclk_cfg = OUT_I2S_ ##num## _MCLK_CFG_OFFSET, \ + .bf_destch_ctrl = BF_DST_CTRL ##num## _OFFSET, \ + .bf_destch_cfg = BF_DST_CFG ##num## _OFFSET, \ + .bf_sourcech_ctrl = BF_SRC_CTRL ##num## _OFFSET, \ + .bf_sourcech_cfg = BF_SRC_CFG ##num## _OFFSET, \ + .bf_sourcech_grp = BF_SRC_GRP ##num## _OFFSET \ +} + +struct pll_macro_entry { + u32 mclk; + u32 pll_ch_num; +}; + +/* + * PLL has 3 output channels (1x, 2x, and 4x). Below are + * the common MCLK frequencies used by audio driver + */ +static const struct pll_macro_entry pll_predef_mclk[] = { + { 4096000, 0}, + { 8192000, 1}, + {16384000, 2}, + + { 5644800, 0}, + {11289600, 1}, + {22579200, 2}, + + { 6144000, 0}, + {12288000, 1}, + {24576000, 2}, + + {12288000, 0}, + {24576000, 1}, + {49152000, 2}, + + {22579200, 0}, + {45158400, 1}, + {90316800, 2}, + + {24576000, 0}, + {49152000, 1}, + {98304000, 2}, +}; + +#define CYGNUS_RATE_MIN 8000 +#define CYGNUS_RATE_MAX 384000 + +/* List of valid frame sizes for tdm mode */ +static const int ssp_valid_tdm_framesize[] = {32, 64, 128, 256, 512}; + +static const unsigned int cygnus_rates[] = { + 8000, 11025, 16000, 22050, 32000, 44100, 48000, + 88200, 96000, 176400, 192000, 352800, 384000 +}; + +static const struct snd_pcm_hw_constraint_list cygnus_rate_constraint = { + .count = ARRAY_SIZE(cygnus_rates), + .list = cygnus_rates, +}; + +static struct cygnus_aio_port *cygnus_dai_get_portinfo(struct snd_soc_dai *dai) +{ + struct cygnus_audio *cygaud = snd_soc_dai_get_drvdata(dai); + + return &cygaud->portinfo[dai->id]; +} + +static int audio_ssp_init_portregs(struct cygnus_aio_port *aio) +{ + u32 value, fci_id; + int status = 0; + + switch (aio->port_type) { + case PORT_TDM: + value = readl(aio->cygaud->audio + aio->regs.i2s_stream_cfg); + value &= ~I2S_STREAM_CFG_MASK; + + /* Set Group ID */ + writel(aio->portnum, + aio->cygaud->audio + aio->regs.bf_sourcech_grp); + + /* Configure the AUD_FMM_IOP_OUT_I2S_x_STREAM_CFG reg */ + value |= aio->portnum << I2S_OUT_STREAM_CFG_GROUP_ID; + value |= aio->portnum; /* FCI ID is the port num */ + value |= CH_GRP_STEREO << I2S_OUT_STREAM_CFG_CHANNEL_GROUPING; + writel(value, aio->cygaud->audio + aio->regs.i2s_stream_cfg); + + /* Configure the AUD_FMM_BF_CTRL_SOURCECH_CFGX reg */ + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~BIT(BF_SRC_CFGX_NOT_PAUSE_WHEN_EMPTY); + value |= BIT(BF_SRC_CFGX_SFIFO_SZ_DOUBLE); + value |= BIT(BF_SRC_CFGX_PROCESS_SEQ_ID_VALID); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + /* Configure the AUD_FMM_IOP_IN_I2S_x_CAP_STREAM_CFG_0 reg */ + value = readl(aio->cygaud->i2s_in + + aio->regs.i2s_cap_stream_cfg); + value &= ~I2S_CAP_STREAM_CFG_MASK; + value |= aio->portnum << I2S_IN_STREAM_CFG_0_GROUP_ID; + writel(value, aio->cygaud->i2s_in + + aio->regs.i2s_cap_stream_cfg); + + /* Configure the AUD_FMM_BF_CTRL_DESTCH_CFGX_REG_BASE reg */ + fci_id = CAPTURE_FCI_ID_BASE + aio->portnum; + + value = readl(aio->cygaud->audio + aio->regs.bf_destch_cfg); + value |= BIT(BF_DST_CFGX_DFIFO_SZ_DOUBLE); + value &= ~BIT(BF_DST_CFGX_NOT_PAUSE_WHEN_FULL); + value |= (fci_id << BF_DST_CFGX_FCI_ID); + value |= BIT(BF_DST_CFGX_PROC_SEQ_ID_VALID); + writel(value, aio->cygaud->audio + aio->regs.bf_destch_cfg); + + /* Enable the transmit pin for this port */ + value = readl(aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + value &= ~BIT((aio->portnum * 4) + AUD_MISC_SEROUT_SDAT_OE); + writel(value, aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + break; + case PORT_SPDIF: + writel(aio->portnum, aio->cygaud->audio + BF_SRC_GRP3_OFFSET); + + value = readl(aio->cygaud->audio + SPDIF_CTRL_OFFSET); + value |= BIT(SPDIF_0_OUT_DITHER_ENA); + writel(value, aio->cygaud->audio + SPDIF_CTRL_OFFSET); + + /* Enable and set the FCI ID for the SPDIF channel */ + value = readl(aio->cygaud->audio + SPDIF_STREAM_CFG_OFFSET); + value &= ~SPDIF_STREAM_CFG_MASK; + value |= aio->portnum; /* FCI ID is the port num */ + value |= BIT(SPDIF_0_OUT_STREAM_ENA); + writel(value, aio->cygaud->audio + SPDIF_STREAM_CFG_OFFSET); + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~BIT(BF_SRC_CFGX_NOT_PAUSE_WHEN_EMPTY); + value |= BIT(BF_SRC_CFGX_SFIFO_SZ_DOUBLE); + value |= BIT(BF_SRC_CFGX_PROCESS_SEQ_ID_VALID); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + /* Enable the spdif output pin */ + value = readl(aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + value &= ~BIT(AUD_MISC_SEROUT_SPDIF_OE); + writel(value, aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + break; + default: + dev_err(aio->cygaud->dev, "Port not supported\n"); + status = -EINVAL; + } + + return status; +} + +static void audio_ssp_in_enable(struct cygnus_aio_port *aio) +{ + u32 value; + + value = readl(aio->cygaud->audio + aio->regs.bf_destch_cfg); + value |= BIT(BF_DST_CFGX_CAP_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_destch_cfg); + + writel(0x1, aio->cygaud->audio + aio->regs.bf_destch_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value |= BIT(I2S_OUT_CFGX_CLK_ENA); + value |= BIT(I2S_OUT_CFGX_DATA_ENABLE); + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + + value = readl(aio->cygaud->i2s_in + aio->regs.i2s_cap_stream_cfg); + value |= BIT(I2S_IN_STREAM_CFG_CAP_ENA); + writel(value, aio->cygaud->i2s_in + aio->regs.i2s_cap_stream_cfg); + + aio->streams_on |= CAPTURE_STREAM_MASK; +} + +static void audio_ssp_in_disable(struct cygnus_aio_port *aio) +{ + u32 value; + + value = readl(aio->cygaud->i2s_in + aio->regs.i2s_cap_stream_cfg); + value &= ~BIT(I2S_IN_STREAM_CFG_CAP_ENA); + writel(value, aio->cygaud->i2s_in + aio->regs.i2s_cap_stream_cfg); + + aio->streams_on &= ~CAPTURE_STREAM_MASK; + + /* If both playback and capture are off */ + if (!aio->streams_on) { + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value &= ~BIT(I2S_OUT_CFGX_CLK_ENA); + value &= ~BIT(I2S_OUT_CFGX_DATA_ENABLE); + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + } + + writel(0x0, aio->cygaud->audio + aio->regs.bf_destch_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.bf_destch_cfg); + value &= ~BIT(BF_DST_CFGX_CAP_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_destch_cfg); +} + +static int audio_ssp_out_enable(struct cygnus_aio_port *aio) +{ + u32 value; + int status = 0; + + switch (aio->port_type) { + case PORT_TDM: + value = readl(aio->cygaud->audio + aio->regs.i2s_stream_cfg); + value |= BIT(I2S_OUT_STREAM_ENA); + writel(value, aio->cygaud->audio + aio->regs.i2s_stream_cfg); + + writel(1, aio->cygaud->audio + aio->regs.bf_sourcech_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value |= BIT(I2S_OUT_CFGX_CLK_ENA); + value |= BIT(I2S_OUT_CFGX_DATA_ENABLE); + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value |= BIT(BF_SRC_CFGX_SFIFO_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + aio->streams_on |= PLAYBACK_STREAM_MASK; + break; + case PORT_SPDIF: + value = readl(aio->cygaud->audio + SPDIF_FORMAT_CFG_OFFSET); + value |= 0x3; + writel(value, aio->cygaud->audio + SPDIF_FORMAT_CFG_OFFSET); + + writel(1, aio->cygaud->audio + aio->regs.bf_sourcech_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value |= BIT(BF_SRC_CFGX_SFIFO_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + break; + default: + dev_err(aio->cygaud->dev, + "Port not supported %d\n", aio->portnum); + status = -EINVAL; + } + + return status; +} + +static int audio_ssp_out_disable(struct cygnus_aio_port *aio) +{ + u32 value; + int status = 0; + + switch (aio->port_type) { + case PORT_TDM: + aio->streams_on &= ~PLAYBACK_STREAM_MASK; + + /* If both playback and capture are off */ + if (!aio->streams_on) { + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value &= ~BIT(I2S_OUT_CFGX_CLK_ENA); + value &= ~BIT(I2S_OUT_CFGX_DATA_ENABLE); + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + } + + /* set group_sync_dis = 1 */ + value = readl(aio->cygaud->audio + BF_SRC_GRP_SYNC_DIS_OFFSET); + value |= BIT(aio->portnum); + writel(value, aio->cygaud->audio + BF_SRC_GRP_SYNC_DIS_OFFSET); + + writel(0, aio->cygaud->audio + aio->regs.bf_sourcech_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~BIT(BF_SRC_CFGX_SFIFO_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + /* set group_sync_dis = 0 */ + value = readl(aio->cygaud->audio + BF_SRC_GRP_SYNC_DIS_OFFSET); + value &= ~BIT(aio->portnum); + writel(value, aio->cygaud->audio + BF_SRC_GRP_SYNC_DIS_OFFSET); + + value = readl(aio->cygaud->audio + aio->regs.i2s_stream_cfg); + value &= ~BIT(I2S_OUT_STREAM_ENA); + writel(value, aio->cygaud->audio + aio->regs.i2s_stream_cfg); + + /* IOP SW INIT on OUT_I2S_x */ + value = readl(aio->cygaud->i2s_in + IOP_SW_INIT_LOGIC); + value |= BIT(aio->portnum); + writel(value, aio->cygaud->i2s_in + IOP_SW_INIT_LOGIC); + value &= ~BIT(aio->portnum); + writel(value, aio->cygaud->i2s_in + IOP_SW_INIT_LOGIC); + break; + case PORT_SPDIF: + value = readl(aio->cygaud->audio + SPDIF_FORMAT_CFG_OFFSET); + value &= ~0x3; + writel(value, aio->cygaud->audio + SPDIF_FORMAT_CFG_OFFSET); + writel(0, aio->cygaud->audio + aio->regs.bf_sourcech_ctrl); + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~BIT(BF_SRC_CFGX_SFIFO_ENA); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + break; + default: + dev_err(aio->cygaud->dev, + "Port not supported %d\n", aio->portnum); + status = -EINVAL; + } + + return status; +} + +static int pll_configure_mclk(struct cygnus_audio *cygaud, u32 mclk, + struct cygnus_aio_port *aio) +{ + int i = 0, error; + bool found = false; + const struct pll_macro_entry *p_entry; + struct clk *ch_clk; + + for (i = 0; i < ARRAY_SIZE(pll_predef_mclk); i++) { + p_entry = &pll_predef_mclk[i]; + if (p_entry->mclk == mclk) { + found = true; + break; + } + } + if (!found) { + dev_err(cygaud->dev, + "%s No valid mclk freq (%u) found!\n", __func__, mclk); + return -EINVAL; + } + + ch_clk = cygaud->audio_clk[p_entry->pll_ch_num]; + + if ((aio->clk_trace.cap_en) && (!aio->clk_trace.cap_clk_en)) { + error = clk_prepare_enable(ch_clk); + if (error) { + dev_err(cygaud->dev, "%s clk_prepare_enable failed %d\n", + __func__, error); + return error; + } + aio->clk_trace.cap_clk_en = true; + } + + if ((aio->clk_trace.play_en) && (!aio->clk_trace.play_clk_en)) { + error = clk_prepare_enable(ch_clk); + if (error) { + dev_err(cygaud->dev, "%s clk_prepare_enable failed %d\n", + __func__, error); + return error; + } + aio->clk_trace.play_clk_en = true; + } + + error = clk_set_rate(ch_clk, mclk); + if (error) { + dev_err(cygaud->dev, "%s Set MCLK rate failed: %d\n", + __func__, error); + return error; + } + + return p_entry->pll_ch_num; +} + +static int cygnus_ssp_set_clocks(struct cygnus_aio_port *aio) +{ + u32 value; + u32 mask = 0xf; + u32 sclk; + u32 mclk_rate; + unsigned int bit_rate; + unsigned int ratio; + + bit_rate = aio->bit_per_frame * aio->lrclk; + + /* + * Check if the bit clock can be generated from the given MCLK. + * MCLK must be a perfect multiple of bit clock and must be one of the + * following values... (2,4,6,8,10,12,14) + */ + if ((aio->mclk % bit_rate) != 0) + return -EINVAL; + + ratio = aio->mclk / bit_rate; + switch (ratio) { + case 2: + case 4: + case 6: + case 8: + case 10: + case 12: + case 14: + mclk_rate = ratio / 2; + break; + + default: + dev_err(aio->cygaud->dev, + "Invalid combination of MCLK and BCLK\n"); + dev_err(aio->cygaud->dev, "lrclk = %u, bits/frame = %u, mclk = %u\n", + aio->lrclk, aio->bit_per_frame, aio->mclk); + return -EINVAL; + } + + /* Set sclk rate */ + switch (aio->port_type) { + case PORT_TDM: + sclk = aio->bit_per_frame; + if (sclk == 512) + sclk = 0; + + /* sclks_per_1fs_div = sclk cycles/32 */ + sclk /= 32; + + /* Set number of bitclks per frame */ + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value &= ~(mask << I2S_OUT_CFGX_SCLKS_PER_1FS_DIV32); + value |= sclk << I2S_OUT_CFGX_SCLKS_PER_1FS_DIV32; + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + dev_dbg(aio->cygaud->dev, + "SCLKS_PER_1FS_DIV32 = 0x%x\n", value); + break; + case PORT_SPDIF: + break; + default: + dev_err(aio->cygaud->dev, "Unknown port type\n"); + return -EINVAL; + } + + /* Set MCLK_RATE ssp port (spdif and ssp are the same) */ + value = readl(aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + value &= ~(0xf << I2S_OUT_MCLKRATE_SHIFT); + value |= (mclk_rate << I2S_OUT_MCLKRATE_SHIFT); + writel(value, aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + + dev_dbg(aio->cygaud->dev, "mclk cfg reg = 0x%x\n", value); + dev_dbg(aio->cygaud->dev, "bits per frame = %u, mclk = %u Hz, lrclk = %u Hz\n", + aio->bit_per_frame, aio->mclk, aio->lrclk); + return 0; +} + +static int cygnus_ssp_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(dai); + int rate, bitres; + u32 value; + u32 mask = 0x1f; + int ret = 0; + + dev_dbg(aio->cygaud->dev, "%s port = %d\n", __func__, aio->portnum); + dev_dbg(aio->cygaud->dev, "params_channels %d\n", + params_channels(params)); + dev_dbg(aio->cygaud->dev, "rate %d\n", params_rate(params)); + dev_dbg(aio->cygaud->dev, "format %d\n", params_format(params)); + + rate = params_rate(params); + + switch (aio->mode) { + case CYGNUS_SSPMODE_TDM: + if ((rate == 192000) && (params_channels(params) > 4)) { + dev_err(aio->cygaud->dev, "Cannot run %d channels at %dHz\n", + params_channels(params), rate); + return -EINVAL; + } + break; + case CYGNUS_SSPMODE_I2S: + aio->bit_per_frame = 64; /* I2S must be 64 bit per frame */ + break; + default: + dev_err(aio->cygaud->dev, + "%s port running in unknown mode\n", __func__); + return -EINVAL; + } + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~BIT(BF_SRC_CFGX_BUFFER_PAIR_ENABLE); + value &= ~BIT(BF_SRC_CFGX_SAMPLE_CH_MODE); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S16_LE: + bitres = 16; + break; + + case SNDRV_PCM_FORMAT_S32_LE: + /* 32 bit mode is coded as 0 */ + bitres = 0; + break; + + default: + return -EINVAL; + } + + value = readl(aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + value &= ~(mask << BF_SRC_CFGX_BIT_RES); + value |= (bitres << BF_SRC_CFGX_BIT_RES); + writel(value, aio->cygaud->audio + aio->regs.bf_sourcech_cfg); + + } else { + + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S16_LE: + value = readl(aio->cygaud->audio + + aio->regs.bf_destch_cfg); + value |= BIT(BF_DST_CFGX_CAP_MODE); + writel(value, aio->cygaud->audio + + aio->regs.bf_destch_cfg); + break; + + case SNDRV_PCM_FORMAT_S32_LE: + value = readl(aio->cygaud->audio + + aio->regs.bf_destch_cfg); + value &= ~BIT(BF_DST_CFGX_CAP_MODE); + writel(value, aio->cygaud->audio + + aio->regs.bf_destch_cfg); + break; + + default: + return -EINVAL; + } + } + + aio->lrclk = rate; + + if (!aio->is_slave) + ret = cygnus_ssp_set_clocks(aio); + + return ret; +} + +/* + * This function sets the mclk frequency for pll clock + */ +static int cygnus_ssp_set_sysclk(struct snd_soc_dai *dai, + int clk_id, unsigned int freq, int dir) +{ + int sel; + u32 value; + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(dai); + struct cygnus_audio *cygaud = snd_soc_dai_get_drvdata(dai); + + dev_dbg(aio->cygaud->dev, + "%s Enter port = %d\n", __func__, aio->portnum); + sel = pll_configure_mclk(cygaud, freq, aio); + if (sel < 0) { + dev_err(aio->cygaud->dev, + "%s Setting mclk failed.\n", __func__); + return -EINVAL; + } + + aio->mclk = freq; + + dev_dbg(aio->cygaud->dev, "%s Setting MCLKSEL to %d\n", __func__, sel); + value = readl(aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + value &= ~(0xf << I2S_OUT_PLLCLKSEL_SHIFT); + value |= (sel << I2S_OUT_PLLCLKSEL_SHIFT); + writel(value, aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + + return 0; +} + +static int cygnus_ssp_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(dai); + + snd_soc_dai_set_dma_data(dai, substream, aio); + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + aio->clk_trace.play_en = true; + else + aio->clk_trace.cap_en = true; + + substream->runtime->hw.rate_min = CYGNUS_RATE_MIN; + substream->runtime->hw.rate_max = CYGNUS_RATE_MAX; + + snd_pcm_hw_constraint_list(substream->runtime, 0, + SNDRV_PCM_HW_PARAM_RATE, &cygnus_rate_constraint); + return 0; +} + +static void cygnus_ssp_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(dai); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + aio->clk_trace.play_en = false; + else + aio->clk_trace.cap_en = false; + + if (!aio->is_slave) { + u32 val; + + val = readl(aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + val &= CYGNUS_PLLCLKSEL_MASK; + if (val >= ARRAY_SIZE(aio->cygaud->audio_clk)) { + dev_err(aio->cygaud->dev, "Clk index %u is out of bounds\n", + val); + return; + } + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + if (aio->clk_trace.play_clk_en) { + clk_disable_unprepare(aio->cygaud-> + audio_clk[val]); + aio->clk_trace.play_clk_en = false; + } + } else { + if (aio->clk_trace.cap_clk_en) { + clk_disable_unprepare(aio->cygaud-> + audio_clk[val]); + aio->clk_trace.cap_clk_en = false; + } + } + } +} + +/* + * Bit Update Notes + * 31 Yes TDM Mode (1 = TDM, 0 = i2s) + * 30 Yes Slave Mode (1 = Slave, 0 = Master) + * 29:26 No Sclks per frame + * 25:18 Yes FS Width + * 17:14 No Valid Slots + * 13 No Bits (1 = 16 bits, 0 = 32 bits) + * 12:08 No Bits per samp + * 07 Yes Justifcation (1 = LSB, 0 = MSB) + * 06 Yes Alignment (1 = Delay 1 clk, 0 = no delay + * 05 Yes SCLK polarity (1 = Rising, 0 = Falling) + * 04 Yes LRCLK Polarity (1 = High for left, 0 = Low for left) + * 03:02 Yes Reserved - write as zero + * 01 No Data Enable + * 00 No CLK Enable + */ +#define I2S_OUT_CFG_REG_UPDATE_MASK 0x3C03FF03 + +/* Input cfg is same as output, but the FS width is not a valid field */ +#define I2S_IN_CFG_REG_UPDATE_MASK (I2S_OUT_CFG_REG_UPDATE_MASK | 0x03FC0000) + +int cygnus_ssp_set_custom_fsync_width(struct snd_soc_dai *cpu_dai, int len) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(cpu_dai); + + if ((len > 0) && (len < 256)) { + aio->fsync_width = len; + return 0; + } else { + return -EINVAL; + } +} +EXPORT_SYMBOL_GPL(cygnus_ssp_set_custom_fsync_width); + +static int cygnus_ssp_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(cpu_dai); + u32 ssp_curcfg; + u32 ssp_newcfg; + u32 ssp_outcfg; + u32 ssp_incfg; + u32 val; + u32 mask; + + dev_dbg(aio->cygaud->dev, "%s Enter fmt: %x\n", __func__, fmt); + + if (aio->port_type == PORT_SPDIF) + return -EINVAL; + + ssp_newcfg = 0; + + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + ssp_newcfg |= BIT(I2S_OUT_CFGX_SLAVE_MODE); + aio->is_slave = 1; + break; + case SND_SOC_DAIFMT_CBS_CFS: + ssp_newcfg &= ~BIT(I2S_OUT_CFGX_SLAVE_MODE); + aio->is_slave = 0; + break; + default: + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + ssp_newcfg |= BIT(I2S_OUT_CFGX_DATA_ALIGNMENT); + ssp_newcfg |= BIT(I2S_OUT_CFGX_FSYNC_WIDTH); + aio->mode = CYGNUS_SSPMODE_I2S; + break; + + case SND_SOC_DAIFMT_DSP_A: + case SND_SOC_DAIFMT_DSP_B: + ssp_newcfg |= BIT(I2S_OUT_CFGX_TDM_MODE); + + /* DSP_A = data after FS, DSP_B = data during FS */ + if ((fmt & SND_SOC_DAIFMT_FORMAT_MASK) == SND_SOC_DAIFMT_DSP_A) + ssp_newcfg |= BIT(I2S_OUT_CFGX_DATA_ALIGNMENT); + + if ((aio->fsync_width > 0) && (aio->fsync_width < 256)) + ssp_newcfg |= + (aio->fsync_width << I2S_OUT_CFGX_FSYNC_WIDTH); + else + ssp_newcfg |= BIT(I2S_OUT_CFGX_FSYNC_WIDTH); + + aio->mode = CYGNUS_SSPMODE_TDM; + break; + + default: + return -EINVAL; + } + + /* + * SSP out cfg. + * Retain bits we do not want to update, then OR in new bits + */ + ssp_curcfg = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + ssp_outcfg = (ssp_curcfg & I2S_OUT_CFG_REG_UPDATE_MASK) | ssp_newcfg; + writel(ssp_outcfg, aio->cygaud->audio + aio->regs.i2s_cfg); + + /* + * SSP in cfg. + * Retain bits we do not want to update, then OR in new bits + */ + ssp_curcfg = readl(aio->cygaud->i2s_in + aio->regs.i2s_cap_cfg); + ssp_incfg = (ssp_curcfg & I2S_IN_CFG_REG_UPDATE_MASK) | ssp_newcfg; + writel(ssp_incfg, aio->cygaud->i2s_in + aio->regs.i2s_cap_cfg); + + val = readl(aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + + /* + * Configure the word clk and bit clk as output or tristate + * Each port has 4 bits for controlling its pins. + * Shift the mask based upon port number. + */ + mask = BIT(AUD_MISC_SEROUT_LRCK_OE) + | BIT(AUD_MISC_SEROUT_SCLK_OE) + | BIT(AUD_MISC_SEROUT_MCLK_OE); + mask = mask << (aio->portnum * 4); + if (aio->is_slave) + /* Set bit for tri-state */ + val |= mask; + else + /* Clear bit for drive */ + val &= ~mask; + + dev_dbg(aio->cygaud->dev, "%s Set OE bits 0x%x\n", __func__, val); + writel(val, aio->cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + + return 0; +} + +static int cygnus_ssp_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(dai); + struct cygnus_audio *cygaud = snd_soc_dai_get_drvdata(dai); + + dev_dbg(aio->cygaud->dev, + "%s cmd %d at port = %d\n", __func__, cmd, aio->portnum); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + case SNDRV_PCM_TRIGGER_RESUME: + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + audio_ssp_out_enable(aio); + else + audio_ssp_in_enable(aio); + cygaud->active_ports++; + + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + case SNDRV_PCM_TRIGGER_SUSPEND: + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + audio_ssp_out_disable(aio); + else + audio_ssp_in_disable(aio); + cygaud->active_ports--; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int cygnus_set_dai_tdm_slot(struct snd_soc_dai *cpu_dai, + unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(cpu_dai); + u32 value; + int bits_per_slot = 0; /* default to 32-bits per slot */ + int frame_bits; + unsigned int active_slots; + bool found = false; + int i; + + if (tx_mask != rx_mask) { + dev_err(aio->cygaud->dev, + "%s tx_mask must equal rx_mask\n", __func__); + return -EINVAL; + } + + active_slots = hweight32(tx_mask); + + if (active_slots > 16) + return -EINVAL; + + /* Slot value must be even */ + if (active_slots % 2) + return -EINVAL; + + /* We encode 16 slots as 0 in the reg */ + if (active_slots == 16) + active_slots = 0; + + /* Slot Width is either 16 or 32 */ + switch (slot_width) { + case 16: + bits_per_slot = 1; + break; + case 32: + bits_per_slot = 0; + break; + default: + bits_per_slot = 0; + dev_warn(aio->cygaud->dev, + "%s Defaulting Slot Width to 32\n", __func__); + } + + frame_bits = slots * slot_width; + + for (i = 0; i < ARRAY_SIZE(ssp_valid_tdm_framesize); i++) { + if (ssp_valid_tdm_framesize[i] == frame_bits) { + found = true; + break; + } + } + + if (!found) { + dev_err(aio->cygaud->dev, + "%s In TDM mode, frame bits INVALID (%d)\n", + __func__, frame_bits); + return -EINVAL; + } + + aio->bit_per_frame = frame_bits; + + dev_dbg(aio->cygaud->dev, "%s active_slots %u, bits per frame %d\n", + __func__, active_slots, frame_bits); + + /* Set capture side of ssp port */ + value = readl(aio->cygaud->i2s_in + aio->regs.i2s_cap_cfg); + value &= ~(0xf << I2S_OUT_CFGX_VALID_SLOT); + value |= (active_slots << I2S_OUT_CFGX_VALID_SLOT); + value &= ~BIT(I2S_OUT_CFGX_BITS_PER_SLOT); + value |= (bits_per_slot << I2S_OUT_CFGX_BITS_PER_SLOT); + writel(value, aio->cygaud->i2s_in + aio->regs.i2s_cap_cfg); + + /* Set playback side of ssp port */ + value = readl(aio->cygaud->audio + aio->regs.i2s_cfg); + value &= ~(0xf << I2S_OUT_CFGX_VALID_SLOT); + value |= (active_slots << I2S_OUT_CFGX_VALID_SLOT); + value &= ~BIT(I2S_OUT_CFGX_BITS_PER_SLOT); + value |= (bits_per_slot << I2S_OUT_CFGX_BITS_PER_SLOT); + writel(value, aio->cygaud->audio + aio->regs.i2s_cfg); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int __cygnus_ssp_suspend(struct snd_soc_dai *cpu_dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(cpu_dai); + + if (!snd_soc_dai_active(cpu_dai)) + return 0; + + if (!aio->is_slave) { + u32 val; + + val = readl(aio->cygaud->audio + aio->regs.i2s_mclk_cfg); + val &= CYGNUS_PLLCLKSEL_MASK; + if (val >= ARRAY_SIZE(aio->cygaud->audio_clk)) { + dev_err(aio->cygaud->dev, "Clk index %u is out of bounds\n", + val); + return -EINVAL; + } + + if (aio->clk_trace.cap_clk_en) + clk_disable_unprepare(aio->cygaud->audio_clk[val]); + if (aio->clk_trace.play_clk_en) + clk_disable_unprepare(aio->cygaud->audio_clk[val]); + + aio->pll_clk_num = val; + } + + return 0; +} + +static int cygnus_ssp_suspend(struct snd_soc_component *component) +{ + struct snd_soc_dai *dai; + int ret = 0; + + for_each_component_dais(component, dai) + ret |= __cygnus_ssp_suspend(dai); + + return ret; +} + +static int __cygnus_ssp_resume(struct snd_soc_dai *cpu_dai) +{ + struct cygnus_aio_port *aio = cygnus_dai_get_portinfo(cpu_dai); + int error; + + if (!snd_soc_dai_active(cpu_dai)) + return 0; + + if (!aio->is_slave) { + if (aio->clk_trace.cap_clk_en) { + error = clk_prepare_enable(aio->cygaud-> + audio_clk[aio->pll_clk_num]); + if (error) { + dev_err(aio->cygaud->dev, "%s clk_prepare_enable failed\n", + __func__); + return -EINVAL; + } + } + if (aio->clk_trace.play_clk_en) { + error = clk_prepare_enable(aio->cygaud-> + audio_clk[aio->pll_clk_num]); + if (error) { + if (aio->clk_trace.cap_clk_en) + clk_disable_unprepare(aio->cygaud-> + audio_clk[aio->pll_clk_num]); + dev_err(aio->cygaud->dev, "%s clk_prepare_enable failed\n", + __func__); + return -EINVAL; + } + } + } + + return 0; +} + +static int cygnus_ssp_resume(struct snd_soc_component *component) +{ + struct snd_soc_dai *dai; + int ret = 0; + + for_each_component_dais(component, dai) + ret |= __cygnus_ssp_resume(dai); + + return ret; +} + +#else +#define cygnus_ssp_suspend NULL +#define cygnus_ssp_resume NULL +#endif + +static const struct snd_soc_dai_ops cygnus_ssp_dai_ops = { + .startup = cygnus_ssp_startup, + .shutdown = cygnus_ssp_shutdown, + .trigger = cygnus_ssp_trigger, + .hw_params = cygnus_ssp_hw_params, + .set_fmt = cygnus_ssp_set_fmt, + .set_sysclk = cygnus_ssp_set_sysclk, + .set_tdm_slot = cygnus_set_dai_tdm_slot, +}; + +static const struct snd_soc_dai_ops cygnus_spdif_dai_ops = { + .startup = cygnus_ssp_startup, + .shutdown = cygnus_ssp_shutdown, + .trigger = cygnus_ssp_trigger, + .hw_params = cygnus_ssp_hw_params, + .set_sysclk = cygnus_ssp_set_sysclk, +}; + +#define INIT_CPU_DAI(num) { \ + .name = "cygnus-ssp" #num, \ + .playback = { \ + .channels_min = 2, \ + .channels_max = 16, \ + .rates = SNDRV_PCM_RATE_KNOT, \ + .formats = SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S32_LE, \ + }, \ + .capture = { \ + .channels_min = 2, \ + .channels_max = 16, \ + .rates = SNDRV_PCM_RATE_KNOT, \ + .formats = SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S32_LE, \ + }, \ + .ops = &cygnus_ssp_dai_ops, \ +} + +static const struct snd_soc_dai_driver cygnus_ssp_dai_info[] = { + INIT_CPU_DAI(0), + INIT_CPU_DAI(1), + INIT_CPU_DAI(2), +}; + +static const struct snd_soc_dai_driver cygnus_spdif_dai_info = { + .name = "cygnus-spdif", + .playback = { + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_KNOT, + .formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S32_LE, + }, + .ops = &cygnus_spdif_dai_ops, +}; + +static struct snd_soc_dai_driver cygnus_ssp_dai[CYGNUS_MAX_PORTS]; + +static const struct snd_soc_component_driver cygnus_ssp_component = { + .name = "cygnus-audio", + .suspend = cygnus_ssp_suspend, + .resume = cygnus_ssp_resume, +}; + +/* + * Return < 0 if error + * Return 0 if disabled + * Return 1 if enabled and node is parsed successfully + */ +static int parse_ssp_child_node(struct platform_device *pdev, + struct device_node *dn, + struct cygnus_audio *cygaud, + struct snd_soc_dai_driver *p_dai) +{ + struct cygnus_aio_port *aio; + struct cygnus_ssp_regs ssp_regs[3]; + u32 rawval; + int portnum = -1; + enum cygnus_audio_port_type port_type; + + if (of_property_read_u32(dn, "reg", &rawval)) { + dev_err(&pdev->dev, "Missing reg property\n"); + return -EINVAL; + } + + portnum = rawval; + switch (rawval) { + case 0: + ssp_regs[0] = INIT_SSP_REGS(0); + port_type = PORT_TDM; + break; + case 1: + ssp_regs[1] = INIT_SSP_REGS(1); + port_type = PORT_TDM; + break; + case 2: + ssp_regs[2] = INIT_SSP_REGS(2); + port_type = PORT_TDM; + break; + case 3: + port_type = PORT_SPDIF; + break; + default: + dev_err(&pdev->dev, "Bad value for reg %u\n", rawval); + return -EINVAL; + } + + aio = &cygaud->portinfo[portnum]; + aio->cygaud = cygaud; + aio->portnum = portnum; + aio->port_type = port_type; + aio->fsync_width = -1; + + switch (port_type) { + case PORT_TDM: + aio->regs = ssp_regs[portnum]; + *p_dai = cygnus_ssp_dai_info[portnum]; + aio->mode = CYGNUS_SSPMODE_UNKNOWN; + break; + + case PORT_SPDIF: + aio->regs.bf_sourcech_cfg = BF_SRC_CFG3_OFFSET; + aio->regs.bf_sourcech_ctrl = BF_SRC_CTRL3_OFFSET; + aio->regs.i2s_mclk_cfg = SPDIF_MCLK_CFG_OFFSET; + aio->regs.i2s_stream_cfg = SPDIF_STREAM_CFG_OFFSET; + *p_dai = cygnus_spdif_dai_info; + + /* For the purposes of this code SPDIF can be I2S mode */ + aio->mode = CYGNUS_SSPMODE_I2S; + break; + default: + dev_err(&pdev->dev, "Bad value for port_type %d\n", port_type); + return -EINVAL; + } + + dev_dbg(&pdev->dev, "%s portnum = %d\n", __func__, aio->portnum); + aio->streams_on = 0; + aio->cygaud->dev = &pdev->dev; + aio->clk_trace.play_en = false; + aio->clk_trace.cap_en = false; + + audio_ssp_init_portregs(aio); + return 0; +} + +static int audio_clk_init(struct platform_device *pdev, + struct cygnus_audio *cygaud) +{ + int i; + char clk_name[PROP_LEN_MAX]; + + for (i = 0; i < ARRAY_SIZE(cygaud->audio_clk); i++) { + snprintf(clk_name, PROP_LEN_MAX, "ch%d_audio", i); + + cygaud->audio_clk[i] = devm_clk_get(&pdev->dev, clk_name); + if (IS_ERR(cygaud->audio_clk[i])) + return PTR_ERR(cygaud->audio_clk[i]); + } + + return 0; +} + +static int cygnus_ssp_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *child_node; + struct resource *res; + struct cygnus_audio *cygaud; + int err = -EINVAL; + int node_count; + int active_port_count; + + cygaud = devm_kzalloc(dev, sizeof(struct cygnus_audio), GFP_KERNEL); + if (!cygaud) + return -ENOMEM; + + dev_set_drvdata(dev, cygaud); + + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "aud"); + cygaud->audio = devm_ioremap_resource(dev, res); + if (IS_ERR(cygaud->audio)) + return PTR_ERR(cygaud->audio); + + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "i2s_in"); + cygaud->i2s_in = devm_ioremap_resource(dev, res); + if (IS_ERR(cygaud->i2s_in)) + return PTR_ERR(cygaud->i2s_in); + + /* Tri-state all controlable pins until we know that we need them */ + writel(CYGNUS_SSP_TRISTATE_MASK, + cygaud->audio + AUD_MISC_SEROUT_OE_REG_BASE); + + node_count = of_get_child_count(pdev->dev.of_node); + if ((node_count < 1) || (node_count > CYGNUS_MAX_PORTS)) { + dev_err(dev, "child nodes is %d. Must be between 1 and %d\n", + node_count, CYGNUS_MAX_PORTS); + return -EINVAL; + } + + active_port_count = 0; + + for_each_available_child_of_node(pdev->dev.of_node, child_node) { + err = parse_ssp_child_node(pdev, child_node, cygaud, + &cygnus_ssp_dai[active_port_count]); + + /* negative is err, 0 is active and good, 1 is disabled */ + if (err < 0) + return err; + else if (!err) { + dev_dbg(dev, "Activating DAI: %s\n", + cygnus_ssp_dai[active_port_count].name); + active_port_count++; + } + } + + cygaud->dev = dev; + cygaud->active_ports = 0; + + dev_dbg(dev, "Registering %d DAIs\n", active_port_count); + err = devm_snd_soc_register_component(dev, &cygnus_ssp_component, + cygnus_ssp_dai, active_port_count); + if (err) { + dev_err(dev, "snd_soc_register_dai failed\n"); + return err; + } + + cygaud->irq_num = platform_get_irq(pdev, 0); + if (cygaud->irq_num <= 0) + return cygaud->irq_num; + + err = audio_clk_init(pdev, cygaud); + if (err) { + dev_err(dev, "audio clock initialization failed\n"); + return err; + } + + err = cygnus_soc_platform_register(dev, cygaud); + if (err) { + dev_err(dev, "platform reg error %d\n", err); + return err; + } + + return 0; +} + +static int cygnus_ssp_remove(struct platform_device *pdev) +{ + cygnus_soc_platform_unregister(&pdev->dev); + + return 0; +} + +static const struct of_device_id cygnus_ssp_of_match[] = { + { .compatible = "brcm,cygnus-audio" }, + {}, +}; +MODULE_DEVICE_TABLE(of, cygnus_ssp_of_match); + +static struct platform_driver cygnus_ssp_driver = { + .probe = cygnus_ssp_probe, + .remove = cygnus_ssp_remove, + .driver = { + .name = "cygnus-ssp", + .of_match_table = cygnus_ssp_of_match, + }, +}; + +module_platform_driver(cygnus_ssp_driver); + +MODULE_ALIAS("platform:cygnus-ssp"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Broadcom"); +MODULE_DESCRIPTION("Cygnus ASoC SSP Interface"); diff --git a/sound/soc/bcm/cygnus-ssp.h b/sound/soc/bcm/cygnus-ssp.h new file mode 100644 index 000000000..33dd34305 --- /dev/null +++ b/sound/soc/bcm/cygnus-ssp.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014-2015 Broadcom Corporation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation version 2. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#ifndef __CYGNUS_SSP_H__ +#define __CYGNUS_SSP_H__ + +#define CYGNUS_TDM_DAI_MAX_SLOTS 16 + +#define CYGNUS_MAX_PLAYBACK_PORTS 4 +#define CYGNUS_MAX_CAPTURE_PORTS 3 +#define CYGNUS_MAX_I2S_PORTS 3 +#define CYGNUS_MAX_PORTS CYGNUS_MAX_PLAYBACK_PORTS +#define CYGNUS_AUIDO_MAX_NUM_CLKS 3 + +#define CYGNUS_SSP_FRAMEBITS_DIV 1 + +#define CYGNUS_SSPMODE_I2S 0 +#define CYGNUS_SSPMODE_TDM 1 +#define CYGNUS_SSPMODE_UNKNOWN -1 + +#define CYGNUS_SSP_CLKSRC_PLL 0 + +/* Max string length of our dt property names */ +#define PROP_LEN_MAX 40 + +struct ringbuf_regs { + unsigned rdaddr; + unsigned wraddr; + unsigned baseaddr; + unsigned endaddr; + unsigned fmark; /* freemark for play, fullmark for caputure */ + unsigned period_bytes; + unsigned buf_size; +}; + +#define RINGBUF_REG_PLAYBACK(num) ((struct ringbuf_regs) { \ + .rdaddr = SRC_RBUF_ ##num## _RDADDR_OFFSET, \ + .wraddr = SRC_RBUF_ ##num## _WRADDR_OFFSET, \ + .baseaddr = SRC_RBUF_ ##num## _BASEADDR_OFFSET, \ + .endaddr = SRC_RBUF_ ##num## _ENDADDR_OFFSET, \ + .fmark = SRC_RBUF_ ##num## _FREE_MARK_OFFSET, \ + .period_bytes = 0, \ + .buf_size = 0, \ +}) + +#define RINGBUF_REG_CAPTURE(num) ((struct ringbuf_regs) { \ + .rdaddr = DST_RBUF_ ##num## _RDADDR_OFFSET, \ + .wraddr = DST_RBUF_ ##num## _WRADDR_OFFSET, \ + .baseaddr = DST_RBUF_ ##num## _BASEADDR_OFFSET, \ + .endaddr = DST_RBUF_ ##num## _ENDADDR_OFFSET, \ + .fmark = DST_RBUF_ ##num## _FULL_MARK_OFFSET, \ + .period_bytes = 0, \ + .buf_size = 0, \ +}) + +enum cygnus_audio_port_type { + PORT_TDM, + PORT_SPDIF, +}; + +struct cygnus_ssp_regs { + u32 i2s_stream_cfg; + u32 i2s_cfg; + u32 i2s_cap_stream_cfg; + u32 i2s_cap_cfg; + u32 i2s_mclk_cfg; + + u32 bf_destch_ctrl; + u32 bf_destch_cfg; + u32 bf_sourcech_ctrl; + u32 bf_sourcech_cfg; + u32 bf_sourcech_grp; +}; + +struct cygnus_track_clk { + bool cap_en; + bool play_en; + bool cap_clk_en; + bool play_clk_en; +}; + +struct cygnus_aio_port { + int portnum; + int mode; + bool is_slave; + int streams_on; /* will be 0 if both capture and play are off */ + int fsync_width; + int port_type; + + u32 mclk; + u32 lrclk; + u32 bit_per_frame; + u32 pll_clk_num; + + struct cygnus_audio *cygaud; + struct cygnus_ssp_regs regs; + + struct ringbuf_regs play_rb_regs; + struct ringbuf_regs capture_rb_regs; + + struct snd_pcm_substream *play_stream; + struct snd_pcm_substream *capture_stream; + + struct cygnus_track_clk clk_trace; +}; + + +struct cygnus_audio { + struct cygnus_aio_port portinfo[CYGNUS_MAX_PORTS]; + + int irq_num; + void __iomem *audio; + struct device *dev; + void __iomem *i2s_in; + + struct clk *audio_clk[CYGNUS_AUIDO_MAX_NUM_CLKS]; + int active_ports; + unsigned long vco_rate; +}; + +extern int cygnus_ssp_get_mode(struct snd_soc_dai *cpu_dai); +extern int cygnus_ssp_add_pll_tweak_controls(struct snd_soc_pcm_runtime *rtd); +extern int cygnus_ssp_set_custom_fsync_width(struct snd_soc_dai *cpu_dai, + int len); +extern int cygnus_soc_platform_register(struct device *dev, + struct cygnus_audio *cygaud); +extern int cygnus_soc_platform_unregister(struct device *dev); +extern int cygnus_ssp_set_custom_fsync_width(struct snd_soc_dai *cpu_dai, + int len); +#endif |