| // SPDX-License-Identifier: GPL-2.0-only |
| // |
| // Copyright (C) 2020 Intel Corporation. |
| // |
| // Intel KeemBay Platform driver. |
| // |
| |
| #include <linux/bitrev.h> |
| #include <linux/clk.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_device.h> |
| #include <sound/dmaengine_pcm.h> |
| #include <sound/pcm.h> |
| #include <sound/pcm_params.h> |
| #include <sound/soc.h> |
| #include "kmb_platform.h" |
| |
| #define PERIODS_MIN 2 |
| #define PERIODS_MAX 48 |
| #define PERIOD_BYTES_MIN 4096 |
| #define BUFFER_BYTES_MAX (PERIODS_MAX * PERIOD_BYTES_MIN) |
| #define TDM_OPERATION 5 |
| #define I2S_OPERATION 0 |
| #define DATA_WIDTH_CONFIG_BIT 6 |
| #define TDM_CHANNEL_CONFIG_BIT 3 |
| |
| static const struct snd_pcm_hardware kmb_pcm_hardware = { |
| .info = SNDRV_PCM_INFO_INTERLEAVED | |
| SNDRV_PCM_INFO_MMAP | |
| SNDRV_PCM_INFO_MMAP_VALID | |
| SNDRV_PCM_INFO_BATCH | |
| SNDRV_PCM_INFO_BLOCK_TRANSFER, |
| .rates = SNDRV_PCM_RATE_8000 | |
| SNDRV_PCM_RATE_16000 | |
| SNDRV_PCM_RATE_48000, |
| .rate_min = 8000, |
| .rate_max = 48000, |
| .formats = SNDRV_PCM_FMTBIT_S16_LE | |
| SNDRV_PCM_FMTBIT_S24_LE | |
| SNDRV_PCM_FMTBIT_S32_LE | |
| SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE, |
| .channels_min = 2, |
| .channels_max = 2, |
| .buffer_bytes_max = BUFFER_BYTES_MAX, |
| .period_bytes_min = PERIOD_BYTES_MIN, |
| .period_bytes_max = BUFFER_BYTES_MAX / PERIODS_MIN, |
| .periods_min = PERIODS_MIN, |
| .periods_max = PERIODS_MAX, |
| .fifo_size = 16, |
| }; |
| |
| /* |
| * Convert to ADV7511 HDMI hardware format. |
| * ADV7511 HDMI chip need parity bit replaced by block start bit and |
| * with the preamble bits left out. |
| * ALSA IEC958 subframe format: |
| * bit 0-3 = preamble (0x8 = block start) |
| * 4-7 = AUX (=0) |
| * 8-27 = audio data (without AUX if 24bit sample) |
| * 28 = validity |
| * 29 = user data |
| * 30 = channel status |
| * 31 = parity |
| * |
| * ADV7511 IEC958 subframe format: |
| * bit 0-23 = audio data |
| * 24 = validity |
| * 25 = user data |
| * 26 = channel status |
| * 27 = block start |
| * 28-31 = 0 |
| * MSB to LSB bit reverse by software as hardware not supporting it. |
| */ |
| static void hdmi_reformat_iec958(struct snd_pcm_runtime *runtime, |
| struct kmb_i2s_info *kmb_i2s, |
| unsigned int tx_ptr) |
| { |
| u32(*buf)[2] = (void *)runtime->dma_area; |
| unsigned long temp; |
| u32 i, j, sample; |
| |
| for (i = 0; i < kmb_i2s->fifo_th; i++) { |
| j = 0; |
| do { |
| temp = buf[tx_ptr][j]; |
| /* Replace parity with block start*/ |
| assign_bit(31, &temp, (BIT(3) & temp)); |
| sample = bitrev32(temp); |
| buf[tx_ptr][j] = sample << 4; |
| j++; |
| } while (j < 2); |
| tx_ptr++; |
| } |
| } |
| |
| static unsigned int kmb_pcm_tx_fn(struct kmb_i2s_info *kmb_i2s, |
| struct snd_pcm_runtime *runtime, |
| unsigned int tx_ptr, bool *period_elapsed) |
| { |
| unsigned int period_pos = tx_ptr % runtime->period_size; |
| void __iomem *i2s_base = kmb_i2s->i2s_base; |
| void *buf = runtime->dma_area; |
| int i; |
| |
| if (kmb_i2s->iec958_fmt) |
| hdmi_reformat_iec958(runtime, kmb_i2s, tx_ptr); |
| |
| /* KMB i2s uses two separate L/R FIFO */ |
| for (i = 0; i < kmb_i2s->fifo_th; i++) { |
| if (kmb_i2s->config.data_width == 16) { |
| writel(((u16(*)[2])buf)[tx_ptr][0], i2s_base + LRBR_LTHR(0)); |
| writel(((u16(*)[2])buf)[tx_ptr][1], i2s_base + RRBR_RTHR(0)); |
| } else { |
| writel(((u32(*)[2])buf)[tx_ptr][0], i2s_base + LRBR_LTHR(0)); |
| writel(((u32(*)[2])buf)[tx_ptr][1], i2s_base + RRBR_RTHR(0)); |
| } |
| |
| period_pos++; |
| |
| if (++tx_ptr >= runtime->buffer_size) |
| tx_ptr = 0; |
| } |
| |
| *period_elapsed = period_pos >= runtime->period_size; |
| |
| return tx_ptr; |
| } |
| |
| static unsigned int kmb_pcm_rx_fn(struct kmb_i2s_info *kmb_i2s, |
| struct snd_pcm_runtime *runtime, |
| unsigned int rx_ptr, bool *period_elapsed) |
| { |
| unsigned int period_pos = rx_ptr % runtime->period_size; |
| void __iomem *i2s_base = kmb_i2s->i2s_base; |
| int chan = kmb_i2s->config.chan_nr; |
| void *buf = runtime->dma_area; |
| int i, j; |
| |
| /* KMB i2s uses two separate L/R FIFO */ |
| for (i = 0; i < kmb_i2s->fifo_th; i++) { |
| for (j = 0; j < chan / 2; j++) { |
| if (kmb_i2s->config.data_width == 16) { |
| ((u16 *)buf)[rx_ptr * chan + (j * 2)] = |
| readl(i2s_base + LRBR_LTHR(j)); |
| ((u16 *)buf)[rx_ptr * chan + ((j * 2) + 1)] = |
| readl(i2s_base + RRBR_RTHR(j)); |
| } else { |
| ((u32 *)buf)[rx_ptr * chan + (j * 2)] = |
| readl(i2s_base + LRBR_LTHR(j)); |
| ((u32 *)buf)[rx_ptr * chan + ((j * 2) + 1)] = |
| readl(i2s_base + RRBR_RTHR(j)); |
| } |
| } |
| period_pos++; |
| |
| if (++rx_ptr >= runtime->buffer_size) |
| rx_ptr = 0; |
| } |
| |
| *period_elapsed = period_pos >= runtime->period_size; |
| |
| return rx_ptr; |
| } |
| |
| static inline void kmb_i2s_disable_channels(struct kmb_i2s_info *kmb_i2s, |
| u32 stream) |
| { |
| u32 i; |
| |
| /* Disable all channels regardless of configuration*/ |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| for (i = 0; i < MAX_ISR; i++) |
| writel(0, kmb_i2s->i2s_base + TER(i)); |
| } else { |
| for (i = 0; i < MAX_ISR; i++) |
| writel(0, kmb_i2s->i2s_base + RER(i)); |
| } |
| } |
| |
| static inline void kmb_i2s_clear_irqs(struct kmb_i2s_info *kmb_i2s, u32 stream) |
| { |
| struct i2s_clk_config_data *config = &kmb_i2s->config; |
| u32 i; |
| |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| for (i = 0; i < config->chan_nr / 2; i++) |
| readl(kmb_i2s->i2s_base + TOR(i)); |
| } else { |
| for (i = 0; i < config->chan_nr / 2; i++) |
| readl(kmb_i2s->i2s_base + ROR(i)); |
| } |
| } |
| |
| static inline void kmb_i2s_irq_trigger(struct kmb_i2s_info *kmb_i2s, |
| u32 stream, int chan_nr, bool trigger) |
| { |
| u32 i, irq; |
| u32 flag; |
| |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) |
| flag = TX_INT_FLAG; |
| else |
| flag = RX_INT_FLAG; |
| |
| for (i = 0; i < chan_nr / 2; i++) { |
| irq = readl(kmb_i2s->i2s_base + IMR(i)); |
| |
| if (trigger) |
| irq = irq & ~flag; |
| else |
| irq = irq | flag; |
| |
| writel(irq, kmb_i2s->i2s_base + IMR(i)); |
| } |
| } |
| |
| static void kmb_pcm_operation(struct kmb_i2s_info *kmb_i2s, bool playback) |
| { |
| struct snd_pcm_substream *substream; |
| bool period_elapsed; |
| unsigned int new_ptr; |
| unsigned int ptr; |
| |
| if (playback) |
| substream = kmb_i2s->tx_substream; |
| else |
| substream = kmb_i2s->rx_substream; |
| |
| if (!substream || !snd_pcm_running(substream)) |
| return; |
| |
| if (playback) { |
| ptr = kmb_i2s->tx_ptr; |
| new_ptr = kmb_pcm_tx_fn(kmb_i2s, substream->runtime, |
| ptr, &period_elapsed); |
| cmpxchg(&kmb_i2s->tx_ptr, ptr, new_ptr); |
| } else { |
| ptr = kmb_i2s->rx_ptr; |
| new_ptr = kmb_pcm_rx_fn(kmb_i2s, substream->runtime, |
| ptr, &period_elapsed); |
| cmpxchg(&kmb_i2s->rx_ptr, ptr, new_ptr); |
| } |
| |
| if (period_elapsed) |
| snd_pcm_period_elapsed(substream); |
| } |
| |
| static int kmb_pcm_open(struct snd_soc_component *component, |
| struct snd_pcm_substream *substream) |
| { |
| struct snd_pcm_runtime *runtime = substream->runtime; |
| struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); |
| struct kmb_i2s_info *kmb_i2s; |
| |
| kmb_i2s = snd_soc_dai_get_drvdata(asoc_rtd_to_cpu(rtd, 0)); |
| snd_soc_set_runtime_hwparams(substream, &kmb_pcm_hardware); |
| snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); |
| runtime->private_data = kmb_i2s; |
| |
| return 0; |
| } |
| |
| static int kmb_pcm_trigger(struct snd_soc_component *component, |
| struct snd_pcm_substream *substream, int cmd) |
| { |
| struct snd_pcm_runtime *runtime = substream->runtime; |
| struct kmb_i2s_info *kmb_i2s = runtime->private_data; |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| kmb_i2s->tx_ptr = 0; |
| kmb_i2s->tx_substream = substream; |
| } else { |
| kmb_i2s->rx_ptr = 0; |
| kmb_i2s->rx_substream = substream; |
| } |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| kmb_i2s->tx_substream = NULL; |
| else |
| kmb_i2s->rx_substream = NULL; |
| kmb_i2s->iec958_fmt = false; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static irqreturn_t kmb_i2s_irq_handler(int irq, void *dev_id) |
| { |
| struct kmb_i2s_info *kmb_i2s = dev_id; |
| struct i2s_clk_config_data *config = &kmb_i2s->config; |
| irqreturn_t ret = IRQ_NONE; |
| u32 tx_enabled = 0; |
| u32 isr[4]; |
| int i; |
| |
| for (i = 0; i < config->chan_nr / 2; i++) |
| isr[i] = readl(kmb_i2s->i2s_base + ISR(i)); |
| |
| kmb_i2s_clear_irqs(kmb_i2s, SNDRV_PCM_STREAM_PLAYBACK); |
| kmb_i2s_clear_irqs(kmb_i2s, SNDRV_PCM_STREAM_CAPTURE); |
| /* Only check TX interrupt if TX is active */ |
| tx_enabled = readl(kmb_i2s->i2s_base + ITER); |
| |
| /* |
| * Data available. Retrieve samples from FIFO |
| */ |
| |
| /* |
| * 8 channel audio will have isr[0..2] triggered, |
| * reading the specific isr based on the audio configuration, |
| * to avoid reading the buffers too early. |
| */ |
| switch (config->chan_nr) { |
| case 2: |
| if (isr[0] & ISR_RXDA) |
| kmb_pcm_operation(kmb_i2s, false); |
| ret = IRQ_HANDLED; |
| break; |
| case 4: |
| if (isr[1] & ISR_RXDA) |
| kmb_pcm_operation(kmb_i2s, false); |
| ret = IRQ_HANDLED; |
| break; |
| case 8: |
| if (isr[3] & ISR_RXDA) |
| kmb_pcm_operation(kmb_i2s, false); |
| ret = IRQ_HANDLED; |
| break; |
| } |
| |
| for (i = 0; i < config->chan_nr / 2; i++) { |
| /* |
| * Check if TX fifo is empty. If empty fill FIFO with samples |
| */ |
| if ((isr[i] & ISR_TXFE) && tx_enabled) { |
| kmb_pcm_operation(kmb_i2s, true); |
| ret = IRQ_HANDLED; |
| } |
| |
| /* Error Handling: TX */ |
| if (isr[i] & ISR_TXFO) { |
| dev_dbg(kmb_i2s->dev, "TX overrun (ch_id=%d)\n", i); |
| ret = IRQ_HANDLED; |
| } |
| /* Error Handling: RX */ |
| if (isr[i] & ISR_RXFO) { |
| dev_dbg(kmb_i2s->dev, "RX overrun (ch_id=%d)\n", i); |
| ret = IRQ_HANDLED; |
| } |
| } |
| |
| return ret; |
| } |
| |
| static int kmb_platform_pcm_new(struct snd_soc_component *component, |
| struct snd_soc_pcm_runtime *soc_runtime) |
| { |
| size_t size = kmb_pcm_hardware.buffer_bytes_max; |
| /* Use SNDRV_DMA_TYPE_CONTINUOUS as KMB doesn't use PCI sg buffer */ |
| snd_pcm_set_managed_buffer_all(soc_runtime->pcm, |
| SNDRV_DMA_TYPE_CONTINUOUS, |
| NULL, size, size); |
| return 0; |
| } |
| |
| static snd_pcm_uframes_t kmb_pcm_pointer(struct snd_soc_component *component, |
| struct snd_pcm_substream *substream) |
| { |
| struct snd_pcm_runtime *runtime = substream->runtime; |
| struct kmb_i2s_info *kmb_i2s = runtime->private_data; |
| snd_pcm_uframes_t pos; |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| pos = kmb_i2s->tx_ptr; |
| else |
| pos = kmb_i2s->rx_ptr; |
| |
| return pos < runtime->buffer_size ? pos : 0; |
| } |
| |
| static const struct snd_soc_component_driver kmb_component = { |
| .name = "kmb", |
| .pcm_construct = kmb_platform_pcm_new, |
| .open = kmb_pcm_open, |
| .trigger = kmb_pcm_trigger, |
| .pointer = kmb_pcm_pointer, |
| .legacy_dai_naming = 1, |
| }; |
| |
| static const struct snd_soc_component_driver kmb_component_dma = { |
| .name = "kmb", |
| .legacy_dai_naming = 1, |
| }; |
| |
| static int kmb_probe(struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| if (kmb_i2s->use_pio) |
| return 0; |
| |
| snd_soc_dai_init_dma_data(cpu_dai, &kmb_i2s->play_dma_data, |
| &kmb_i2s->capture_dma_data); |
| |
| return 0; |
| } |
| |
| static inline void kmb_i2s_enable_dma(struct kmb_i2s_info *kmb_i2s, u32 stream) |
| { |
| u32 dma_reg; |
| |
| dma_reg = readl(kmb_i2s->i2s_base + I2S_DMACR); |
| /* Enable DMA handshake for stream */ |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) |
| dma_reg |= I2S_DMAEN_TXBLOCK; |
| else |
| dma_reg |= I2S_DMAEN_RXBLOCK; |
| |
| writel(dma_reg, kmb_i2s->i2s_base + I2S_DMACR); |
| } |
| |
| static inline void kmb_i2s_disable_dma(struct kmb_i2s_info *kmb_i2s, u32 stream) |
| { |
| u32 dma_reg; |
| |
| dma_reg = readl(kmb_i2s->i2s_base + I2S_DMACR); |
| /* Disable DMA handshake for stream */ |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| dma_reg &= ~I2S_DMAEN_TXBLOCK; |
| writel(1, kmb_i2s->i2s_base + I2S_RTXDMA); |
| } else { |
| dma_reg &= ~I2S_DMAEN_RXBLOCK; |
| writel(1, kmb_i2s->i2s_base + I2S_RRXDMA); |
| } |
| writel(dma_reg, kmb_i2s->i2s_base + I2S_DMACR); |
| } |
| |
| static void kmb_i2s_start(struct kmb_i2s_info *kmb_i2s, |
| struct snd_pcm_substream *substream) |
| { |
| struct i2s_clk_config_data *config = &kmb_i2s->config; |
| |
| /* I2S Programming sequence in Keem_Bay_VPU_DB_v1.1 */ |
| writel(1, kmb_i2s->i2s_base + IER); |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| writel(1, kmb_i2s->i2s_base + ITER); |
| else |
| writel(1, kmb_i2s->i2s_base + IRER); |
| |
| if (kmb_i2s->use_pio) |
| kmb_i2s_irq_trigger(kmb_i2s, substream->stream, |
| config->chan_nr, true); |
| else |
| kmb_i2s_enable_dma(kmb_i2s, substream->stream); |
| |
| if (kmb_i2s->clock_provider) |
| writel(1, kmb_i2s->i2s_base + CER); |
| else |
| writel(0, kmb_i2s->i2s_base + CER); |
| } |
| |
| static void kmb_i2s_stop(struct kmb_i2s_info *kmb_i2s, |
| struct snd_pcm_substream *substream) |
| { |
| /* I2S Programming sequence in Keem_Bay_VPU_DB_v1.1 */ |
| kmb_i2s_clear_irqs(kmb_i2s, substream->stream); |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| writel(0, kmb_i2s->i2s_base + ITER); |
| else |
| writel(0, kmb_i2s->i2s_base + IRER); |
| |
| kmb_i2s_irq_trigger(kmb_i2s, substream->stream, 8, false); |
| |
| if (!kmb_i2s->active) { |
| writel(0, kmb_i2s->i2s_base + CER); |
| writel(0, kmb_i2s->i2s_base + IER); |
| } |
| } |
| |
| static void kmb_disable_clk(void *clk) |
| { |
| clk_disable_unprepare(clk); |
| } |
| |
| static int kmb_set_dai_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| int ret; |
| |
| switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { |
| case SND_SOC_DAIFMT_BC_FC: |
| kmb_i2s->clock_provider = false; |
| ret = 0; |
| break; |
| case SND_SOC_DAIFMT_BP_FP: |
| writel(CLOCK_PROVIDER_MODE, kmb_i2s->pss_base + I2S_GEN_CFG_0); |
| |
| ret = clk_prepare_enable(kmb_i2s->clk_i2s); |
| if (ret < 0) |
| return ret; |
| |
| ret = devm_add_action_or_reset(kmb_i2s->dev, kmb_disable_clk, |
| kmb_i2s->clk_i2s); |
| if (ret) |
| return ret; |
| |
| kmb_i2s->clock_provider = true; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return ret; |
| } |
| |
| static int kmb_dai_trigger(struct snd_pcm_substream *substream, |
| int cmd, struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| /* Keep track of i2s activity before turn off |
| * the i2s interface |
| */ |
| kmb_i2s->active++; |
| kmb_i2s_start(kmb_i2s, substream); |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| kmb_i2s->active--; |
| if (kmb_i2s->use_pio) |
| kmb_i2s_stop(kmb_i2s, substream); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static void kmb_i2s_config(struct kmb_i2s_info *kmb_i2s, int stream) |
| { |
| struct i2s_clk_config_data *config = &kmb_i2s->config; |
| u32 ch_reg; |
| |
| kmb_i2s_disable_channels(kmb_i2s, stream); |
| |
| for (ch_reg = 0; ch_reg < config->chan_nr / 2; ch_reg++) { |
| if (stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| writel(kmb_i2s->xfer_resolution, |
| kmb_i2s->i2s_base + TCR(ch_reg)); |
| |
| writel(kmb_i2s->fifo_th - 1, |
| kmb_i2s->i2s_base + TFCR(ch_reg)); |
| |
| writel(1, kmb_i2s->i2s_base + TER(ch_reg)); |
| } else { |
| writel(kmb_i2s->xfer_resolution, |
| kmb_i2s->i2s_base + RCR(ch_reg)); |
| |
| writel(kmb_i2s->fifo_th - 1, |
| kmb_i2s->i2s_base + RFCR(ch_reg)); |
| |
| writel(1, kmb_i2s->i2s_base + RER(ch_reg)); |
| } |
| } |
| } |
| |
| static int kmb_dai_hw_params(struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *hw_params, |
| struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| struct i2s_clk_config_data *config = &kmb_i2s->config; |
| u32 write_val; |
| int ret; |
| |
| switch (params_format(hw_params)) { |
| case SNDRV_PCM_FORMAT_S16_LE: |
| config->data_width = 16; |
| kmb_i2s->ccr = 0x00; |
| kmb_i2s->xfer_resolution = 0x02; |
| kmb_i2s->play_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; |
| kmb_i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; |
| break; |
| case SNDRV_PCM_FORMAT_S24_LE: |
| config->data_width = 32; |
| kmb_i2s->ccr = 0x14; |
| kmb_i2s->xfer_resolution = 0x05; |
| kmb_i2s->play_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| kmb_i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| break; |
| case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE: |
| kmb_i2s->iec958_fmt = true; |
| fallthrough; |
| case SNDRV_PCM_FORMAT_S32_LE: |
| config->data_width = 32; |
| kmb_i2s->ccr = 0x10; |
| kmb_i2s->xfer_resolution = 0x05; |
| kmb_i2s->play_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| kmb_i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| break; |
| default: |
| dev_err(kmb_i2s->dev, "kmb: unsupported PCM fmt"); |
| return -EINVAL; |
| } |
| |
| config->chan_nr = params_channels(hw_params); |
| |
| switch (config->chan_nr) { |
| case 8: |
| case 4: |
| /* |
| * Platform is not capable of providing clocks for |
| * multi channel audio |
| */ |
| if (kmb_i2s->clock_provider) |
| return -EINVAL; |
| |
| write_val = ((config->chan_nr / 2) << TDM_CHANNEL_CONFIG_BIT) | |
| (config->data_width << DATA_WIDTH_CONFIG_BIT) | |
| TDM_OPERATION; |
| |
| writel(write_val, kmb_i2s->pss_base + I2S_GEN_CFG_0); |
| break; |
| case 2: |
| /* |
| * Platform is only capable of providing clocks need for |
| * 2 channel master mode |
| */ |
| if (!(kmb_i2s->clock_provider)) |
| return -EINVAL; |
| |
| write_val = ((config->chan_nr / 2) << TDM_CHANNEL_CONFIG_BIT) | |
| (config->data_width << DATA_WIDTH_CONFIG_BIT) | |
| CLOCK_PROVIDER_MODE | I2S_OPERATION; |
| |
| writel(write_val, kmb_i2s->pss_base + I2S_GEN_CFG_0); |
| break; |
| default: |
| dev_dbg(kmb_i2s->dev, "channel not supported\n"); |
| return -EINVAL; |
| } |
| |
| kmb_i2s_config(kmb_i2s, substream->stream); |
| |
| writel(kmb_i2s->ccr, kmb_i2s->i2s_base + CCR); |
| |
| config->sample_rate = params_rate(hw_params); |
| |
| if (kmb_i2s->clock_provider) { |
| /* Only 2 ch supported in Master mode */ |
| u32 bitclk = config->sample_rate * config->data_width * 2; |
| |
| ret = clk_set_rate(kmb_i2s->clk_i2s, bitclk); |
| if (ret) { |
| dev_err(kmb_i2s->dev, |
| "Can't set I2S clock rate: %d\n", ret); |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int kmb_dai_prepare(struct snd_pcm_substream *substream, |
| struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| writel(1, kmb_i2s->i2s_base + TXFFR); |
| else |
| writel(1, kmb_i2s->i2s_base + RXFFR); |
| |
| return 0; |
| } |
| |
| static int kmb_dai_startup(struct snd_pcm_substream *substream, |
| struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| struct snd_dmaengine_dai_dma_data *dma_data; |
| |
| if (kmb_i2s->use_pio) |
| return 0; |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| dma_data = &kmb_i2s->play_dma_data; |
| else |
| dma_data = &kmb_i2s->capture_dma_data; |
| |
| snd_soc_dai_set_dma_data(cpu_dai, substream, dma_data); |
| |
| return 0; |
| } |
| |
| static int kmb_dai_hw_free(struct snd_pcm_substream *substream, |
| struct snd_soc_dai *cpu_dai) |
| { |
| struct kmb_i2s_info *kmb_i2s = snd_soc_dai_get_drvdata(cpu_dai); |
| /* I2S Programming sequence in Keem_Bay_VPU_DB_v1.1 */ |
| if (kmb_i2s->use_pio) |
| kmb_i2s_clear_irqs(kmb_i2s, substream->stream); |
| |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) |
| writel(0, kmb_i2s->i2s_base + ITER); |
| else |
| writel(0, kmb_i2s->i2s_base + IRER); |
| |
| if (kmb_i2s->use_pio) |
| kmb_i2s_irq_trigger(kmb_i2s, substream->stream, 8, false); |
| else |
| kmb_i2s_disable_dma(kmb_i2s, substream->stream); |
| |
| if (!kmb_i2s->active) { |
| writel(0, kmb_i2s->i2s_base + CER); |
| writel(0, kmb_i2s->i2s_base + IER); |
| } |
| |
| return 0; |
| } |
| |
| static const struct snd_soc_dai_ops kmb_dai_ops = { |
| .startup = kmb_dai_startup, |
| .trigger = kmb_dai_trigger, |
| .hw_params = kmb_dai_hw_params, |
| .hw_free = kmb_dai_hw_free, |
| .prepare = kmb_dai_prepare, |
| .set_fmt = kmb_set_dai_fmt, |
| }; |
| |
| static struct snd_soc_dai_driver intel_kmb_hdmi_dai[] = { |
| { |
| .name = "intel_kmb_hdmi_i2s", |
| .playback = { |
| .channels_min = 2, |
| .channels_max = 2, |
| .rates = SNDRV_PCM_RATE_48000, |
| .rate_min = 48000, |
| .rate_max = 48000, |
| .formats = (SNDRV_PCM_FMTBIT_S16_LE | |
| SNDRV_PCM_FMTBIT_S24_LE | |
| SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE), |
| }, |
| .ops = &kmb_dai_ops, |
| .probe = kmb_probe, |
| }, |
| }; |
| |
| static struct snd_soc_dai_driver intel_kmb_i2s_dai[] = { |
| { |
| .name = "intel_kmb_i2s", |
| .playback = { |
| .channels_min = 2, |
| .channels_max = 2, |
| .rates = SNDRV_PCM_RATE_8000 | |
| SNDRV_PCM_RATE_16000 | |
| SNDRV_PCM_RATE_48000, |
| .rate_min = 8000, |
| .rate_max = 48000, |
| .formats = (SNDRV_PCM_FMTBIT_S32_LE | |
| SNDRV_PCM_FMTBIT_S24_LE | |
| SNDRV_PCM_FMTBIT_S16_LE), |
| }, |
| .capture = { |
| .channels_min = 2, |
| .channels_max = 2, |
| .rates = SNDRV_PCM_RATE_8000 | |
| SNDRV_PCM_RATE_16000 | |
| SNDRV_PCM_RATE_48000, |
| .rate_min = 8000, |
| .rate_max = 48000, |
| .formats = (SNDRV_PCM_FMTBIT_S32_LE | |
| SNDRV_PCM_FMTBIT_S24_LE | |
| SNDRV_PCM_FMTBIT_S16_LE), |
| }, |
| .ops = &kmb_dai_ops, |
| .probe = kmb_probe, |
| }, |
| }; |
| |
| static struct snd_soc_dai_driver intel_kmb_tdm_dai[] = { |
| { |
| .name = "intel_kmb_tdm", |
| .capture = { |
| .channels_min = 4, |
| .channels_max = 8, |
| .rates = SNDRV_PCM_RATE_8000 | |
| SNDRV_PCM_RATE_16000 | |
| SNDRV_PCM_RATE_48000, |
| .rate_min = 8000, |
| .rate_max = 48000, |
| .formats = (SNDRV_PCM_FMTBIT_S32_LE | |
| SNDRV_PCM_FMTBIT_S24_LE | |
| SNDRV_PCM_FMTBIT_S16_LE), |
| }, |
| .ops = &kmb_dai_ops, |
| .probe = kmb_probe, |
| }, |
| }; |
| |
| static const struct of_device_id kmb_plat_of_match[] = { |
| { .compatible = "intel,keembay-i2s", .data = &intel_kmb_i2s_dai}, |
| { .compatible = "intel,keembay-hdmi-i2s", .data = &intel_kmb_hdmi_dai}, |
| { .compatible = "intel,keembay-tdm", .data = &intel_kmb_tdm_dai}, |
| {} |
| }; |
| |
| static int kmb_plat_dai_probe(struct platform_device *pdev) |
| { |
| struct device_node *np = pdev->dev.of_node; |
| struct snd_soc_dai_driver *kmb_i2s_dai; |
| const struct of_device_id *match; |
| struct device *dev = &pdev->dev; |
| struct kmb_i2s_info *kmb_i2s; |
| struct resource *res; |
| int ret, irq; |
| u32 comp1_reg; |
| |
| kmb_i2s = devm_kzalloc(dev, sizeof(*kmb_i2s), GFP_KERNEL); |
| if (!kmb_i2s) |
| return -ENOMEM; |
| |
| kmb_i2s_dai = devm_kzalloc(dev, sizeof(*kmb_i2s_dai), GFP_KERNEL); |
| if (!kmb_i2s_dai) |
| return -ENOMEM; |
| |
| match = of_match_device(kmb_plat_of_match, &pdev->dev); |
| if (!match) { |
| dev_err(&pdev->dev, "Error: No device match found\n"); |
| return -ENODEV; |
| } |
| kmb_i2s_dai = (struct snd_soc_dai_driver *) match->data; |
| |
| /* Prepare the related clocks */ |
| kmb_i2s->clk_apb = devm_clk_get(dev, "apb_clk"); |
| if (IS_ERR(kmb_i2s->clk_apb)) { |
| dev_err(dev, "Failed to get apb clock\n"); |
| return PTR_ERR(kmb_i2s->clk_apb); |
| } |
| |
| ret = clk_prepare_enable(kmb_i2s->clk_apb); |
| if (ret < 0) |
| return ret; |
| |
| ret = devm_add_action_or_reset(dev, kmb_disable_clk, kmb_i2s->clk_apb); |
| if (ret) { |
| dev_err(dev, "Failed to add clk_apb reset action\n"); |
| return ret; |
| } |
| |
| kmb_i2s->clk_i2s = devm_clk_get(dev, "osc"); |
| if (IS_ERR(kmb_i2s->clk_i2s)) { |
| dev_err(dev, "Failed to get osc clock\n"); |
| return PTR_ERR(kmb_i2s->clk_i2s); |
| } |
| |
| kmb_i2s->i2s_base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); |
| if (IS_ERR(kmb_i2s->i2s_base)) |
| return PTR_ERR(kmb_i2s->i2s_base); |
| |
| kmb_i2s->pss_base = devm_platform_ioremap_resource(pdev, 1); |
| if (IS_ERR(kmb_i2s->pss_base)) |
| return PTR_ERR(kmb_i2s->pss_base); |
| |
| kmb_i2s->dev = &pdev->dev; |
| |
| comp1_reg = readl(kmb_i2s->i2s_base + I2S_COMP_PARAM_1); |
| |
| kmb_i2s->fifo_th = (1 << COMP1_FIFO_DEPTH(comp1_reg)) / 2; |
| |
| kmb_i2s->use_pio = !(of_property_read_bool(np, "dmas")); |
| |
| if (kmb_i2s->use_pio) { |
| irq = platform_get_irq_optional(pdev, 0); |
| if (irq > 0) { |
| ret = devm_request_irq(dev, irq, kmb_i2s_irq_handler, 0, |
| pdev->name, kmb_i2s); |
| if (ret < 0) { |
| dev_err(dev, "failed to request irq\n"); |
| return ret; |
| } |
| } |
| ret = devm_snd_soc_register_component(dev, &kmb_component, |
| kmb_i2s_dai, 1); |
| } else { |
| kmb_i2s->play_dma_data.addr = res->start + I2S_TXDMA; |
| kmb_i2s->capture_dma_data.addr = res->start + I2S_RXDMA; |
| ret = snd_dmaengine_pcm_register(&pdev->dev, |
| NULL, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "could not register dmaengine: %d\n", |
| ret); |
| return ret; |
| } |
| ret = devm_snd_soc_register_component(dev, &kmb_component_dma, |
| kmb_i2s_dai, 1); |
| } |
| |
| if (ret) { |
| dev_err(dev, "not able to register dai\n"); |
| return ret; |
| } |
| |
| /* To ensure none of the channels are enabled at boot up */ |
| kmb_i2s_disable_channels(kmb_i2s, SNDRV_PCM_STREAM_PLAYBACK); |
| kmb_i2s_disable_channels(kmb_i2s, SNDRV_PCM_STREAM_CAPTURE); |
| |
| dev_set_drvdata(dev, kmb_i2s); |
| |
| return ret; |
| } |
| |
| static struct platform_driver kmb_plat_dai_driver = { |
| .driver = { |
| .name = "kmb-plat-dai", |
| .of_match_table = kmb_plat_of_match, |
| }, |
| .probe = kmb_plat_dai_probe, |
| }; |
| |
| module_platform_driver(kmb_plat_dai_driver); |
| |
| MODULE_DESCRIPTION("ASoC Intel KeemBay Platform driver"); |
| MODULE_AUTHOR("Sia Jee Heng <jee.heng.sia@intel.com>"); |
| MODULE_AUTHOR("Sit, Michael Wei Hong <michael.wei.hong.sit@intel.com>"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("platform:kmb_platform"); |