[alsa-devel] [PATCH 1/3] ASoC: xlnx: add Xilinx logicPD-I2S FPGA IP support
Michal Simek
michal.simek at xilinx.com
Mon Sep 2 09:39:11 CEST 2019
Hi Miquel
On 30. 08. 19 23:06, Miquel Raynal wrote:
> This IP is very simple so this driver manage both the DAI and the PCM
> streams, hence the presence of both components in this driver.
>
> There are plenty available interruptions when capturing or playing
> back audio that can be triggered but the only one that fits the ALSA
> sound system is the XFER_DONE which is used to bound sound
> periods. Other interrupts are masked. Please note that capture and
> playback are not possible at the same time though.
>
> Capture seems to work (at least it creates a file with something
> inside) but I have no capture mechanism on the board to actually test
> that it works correctly.
>
> Signed-off-by: Miquel Raynal <miquel.raynal at bootlin.com>
> ---
>
> Hello,
>
> This is my first contribution in the sound subsystem, I hope I've
> understood the core but I might be entirely wrong as well, so please
> do not hesitate to be critical on my choices.
>
> Thanks,
> Miquèl
>
> sound/soc/xilinx/Kconfig | 7 +
> sound/soc/xilinx/Makefile | 2 +
> sound/soc/xilinx/xlnx-logicpd-i2s.c | 468 ++++++++++++++++++++++++++++
What IP is this?
https://www.xilinx.com/products/intellectual-property/audio-i2s.html
https://github.com/Xilinx/linux-xlnx/blob/master/sound/soc/xilinx/xlnx_i2s.c
Anyway I am adding Praveen and Maruthi to take a look.
Thanks,
Michal
> 3 files changed, 477 insertions(+)
> create mode 100644 sound/soc/xilinx/xlnx-logicpd-i2s.c
>
> diff --git a/sound/soc/xilinx/Kconfig b/sound/soc/xilinx/Kconfig
> index 47f606b924e4..b62cae6750b9 100644
> --- a/sound/soc/xilinx/Kconfig
> +++ b/sound/soc/xilinx/Kconfig
> @@ -7,6 +7,13 @@ config SND_SOC_XILINX_I2S
> PCM data. In receiver mode, IP receives PCM audio and
> encapsulates PCM in AES format and sends AES data.
>
> +config SND_SOC_XILINX_LOGICPD_I2S
> + tristate "Audio support for the Xilinx logicPD I2S"
> + help
> + Select this option to enable Xilinx logicPD I2S slave
> + transceiver. This enables I2S playback and capture using
> + Xilinx/logicPD IP.
> +
> config SND_SOC_XILINX_AUDIO_FORMATTER
> tristate "Audio support for the the Xilinx audio formatter"
> help
> diff --git a/sound/soc/xilinx/Makefile b/sound/soc/xilinx/Makefile
> index d79fd38b094b..d127c30f8fe2 100644
> --- a/sound/soc/xilinx/Makefile
> +++ b/sound/soc/xilinx/Makefile
> @@ -1,5 +1,7 @@
> snd-soc-xlnx-i2s-objs := xlnx_i2s.o
> obj-$(CONFIG_SND_SOC_XILINX_I2S) += snd-soc-xlnx-i2s.o
> +snd-soc-xlnx-logicpd-i2s-objs := xlnx-logicpd-i2s.o
> +obj-$(CONFIG_SND_SOC_XILINX_LOGICPD_I2S) += snd-soc-xlnx-logicpd-i2s.o
> snd-soc-xlnx-formatter-pcm-objs := xlnx_formatter_pcm.o
> obj-$(CONFIG_SND_SOC_XILINX_AUDIO_FORMATTER) += snd-soc-xlnx-formatter-pcm.o
> snd-soc-xlnx-spdif-objs := xlnx_spdif.o
> diff --git a/sound/soc/xilinx/xlnx-logicpd-i2s.c b/sound/soc/xilinx/xlnx-logicpd-i2s.c
> new file mode 100644
> index 000000000000..325a5bb6978a
> --- /dev/null
> +++ b/sound/soc/xilinx/xlnx-logicpd-i2s.c
> @@ -0,0 +1,468 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Xilinx logicPD logiI2S - I2S slave transceiver v2 support
> + *
> + * Copyright (C) 2019 Bootlin
> + *
> + * Author: Miquel Raynal <miquel.raynal at bootlin.com>
> + */
> +
> +#include <linux/dma-mapping.h>
> +#include <linux/io.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/of_platform.h>
> +#include <linux/platform_device.h>
> +#include <sound/dmaengine_pcm.h>
> +#include <sound/pcm_params.h>
> +#include <sound/soc.h>
> +
> +#define DRV_NAME "xlnx_logicpd_i2s"
> +
> +#define IP_VERSION 0x0
> +#define PATCH_LEVEL(reg) (((reg) & GENMASK(4, 0)) + 'a')
> +#define MINOR_REV(reg) (((reg) & GENMASK(10, 5)) >> 5)
> +#define MAJOR_REV(reg) (((reg) & GENMASK(16, 11)) >> 11)
> +#define LICENSE_TYPE(reg) (((reg) & GENMASK(18, 17)) >> 17)
> +#define CONTROL_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0x4 : 0x24)
> +#define ENGINE_EN BIT(0)
> +#define XFER_DONE BIT(1)
> +#define BUFF_BASE_ADDR_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0x8 : 0x28)
> +#define BUFF_LEN_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0xC : 0x2C)
> +#define FIFO_STAT_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0x10 : 0x30)
> +#define INTR_MASK_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0x14 : 0x34)
> +#define XFER_DONE_INTR BIT(31)
> +#define INTR_STAT_REG(s) ((s) == SNDRV_PCM_STREAM_PLAYBACK ? 0x18 : 0x38)
> +#define FIFO_COUNT(reg) ((reg) >> 20)
> +
> +#define BYTES_TO_WORDS(n) ((n) / 4)
> +
> +/* Arbitrarily chosen period size */
> +#define PCM_PERIOD_WORDS SZ_8K
> +#define PCM_PERIOD_BYTES (PCM_PERIOD_WORDS * 4)
> +/* This is the actual maximum size that can actually be moved in one chunk */
> +#define PCM_BUF_WORDS (SZ_64K - 1)
> +#define PCM_BUF_BYTES (PCM_BUF_WORDS * 4)
> +
> +struct xlnx_logicpd_i2s;
> +
> +/**
> + * struct xlnx_logicpd_stream - Internal stream representation
> + *
> + * @i2s: Chip data
> + * @substream: Core substream structure
> + * @period_idx: Index of the period within the circular buffer
> + */
> +struct xlnx_logicpd_stream {
> + struct xlnx_logicpd_i2s *i2s;
> + struct snd_pcm_substream *substream;
> + unsigned int period_idx;
> +};
> +
> +/**
> + * struct xlnx_logicpd_i2s - Chip structure
> + *
> + * @base: Registers base address
> + * @streams: Playback and capture streams in an array
> + */
> +struct xlnx_logicpd_i2s {
> + void __iomem *base;
> + struct xlnx_logicpd_stream streams[2];
> +};
> +
> +static struct xlnx_logicpd_i2s *substream_to_cpu_dai_chip(struct snd_pcm_substream *substream)
> +{
> + struct snd_soc_pcm_runtime *rtd = snd_pcm_substream_chip(substream);
> +
> + return snd_soc_dai_get_drvdata(rtd->cpu_dai);
> +}
> +
> +/* PCM methods */
> +
> +static const struct snd_pcm_hardware xlnx_logicpd_pcm_hardware = {
> + .info = SNDRV_PCM_INFO_MMAP |
> + SNDRV_PCM_INFO_MMAP_VALID |
> + SNDRV_PCM_INFO_INTERLEAVED |
> + SNDRV_PCM_INFO_HALF_DUPLEX,
> + .formats = SNDRV_PCM_FMTBIT_S16_LE,
> + .rates = SNDRV_PCM_RATE_8000_192000,
> + .rate_min = 8000,
> + .rate_max = 192000,
> + .channels_min = 2,
> + .channels_max = 2,
> + .period_bytes_min = 0,
> + .period_bytes_max = PCM_PERIOD_BYTES,
> + .periods_min = 0,
> + .periods_max = -1,
> + .buffer_bytes_max = PCM_BUF_BYTES,
> +};
> +
> +static int xlnx_logicpd_pcm_open(struct snd_pcm_substream *substream)
> +{
> + struct xlnx_logicpd_i2s *i2s = substream_to_cpu_dai_chip(substream);
> + unsigned int dir = substream->stream;
> +
> + snd_soc_set_runtime_hwparams(substream, &xlnx_logicpd_pcm_hardware);
> +
> + i2s->streams[dir].substream = substream;
> +
> + return 0;
> +}
> +
> +static int xlnx_logicpd_pcm_close(struct snd_pcm_substream *substream)
> +{
> + struct xlnx_logicpd_i2s *i2s = substream_to_cpu_dai_chip(substream);
> + unsigned int dir = substream->stream;
> +
> + i2s->streams[dir].substream = NULL;
> +
> + return 0;
> +}
> +
> +static int xlnx_logicpd_pcm_hw_params(struct snd_pcm_substream *substream,
> + struct snd_pcm_hw_params *params)
> +{
> + struct snd_pcm_runtime *runtime = substream->runtime;
> +
> + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
> + runtime->dma_bytes = params_buffer_bytes(params);
> +
> + return 0;
> +}
> +
> +static snd_pcm_uframes_t xlnx_logicpd_pcm_pointer(struct snd_pcm_substream *substream)
> +{
> + struct xlnx_logicpd_i2s *i2s = substream_to_cpu_dai_chip(substream);
> + struct snd_pcm_runtime *runtime = substream->runtime;
> + unsigned int period_sz = snd_pcm_lib_period_bytes(substream);
> + unsigned int dir = substream->stream;
> +
> + return bytes_to_frames(runtime,
> + i2s->streams[dir].period_idx * period_sz);
> +}
> +
> +static int xlnx_logicpd_pcm_mmap(struct snd_pcm_substream *substream,
> + struct vm_area_struct *vma)
> +{
> + return remap_pfn_range(vma, vma->vm_start,
> + substream->dma_buffer.addr >> PAGE_SHIFT,
> + vma->vm_end - vma->vm_start, vma->vm_page_prot);
> +}
> +
> +static const struct snd_pcm_ops xlnx_logicpd_pcm_ops = {
> + .open = xlnx_logicpd_pcm_open,
> + .close = xlnx_logicpd_pcm_close,
> + .ioctl = snd_pcm_lib_ioctl,
> + .hw_params = xlnx_logicpd_pcm_hw_params,
> + .pointer = xlnx_logicpd_pcm_pointer,
> + .mmap = xlnx_logicpd_pcm_mmap,
> +};
> +
> +static int xlnx_logicpd_pcm_new(struct snd_soc_pcm_runtime *rtd)
> +{
> + struct snd_pcm *pcm = rtd->pcm;
> + struct snd_pcm_substream *substream;
> + struct snd_dma_buffer *buf;
> + int dir;
> +
> + for (dir = SNDRV_PCM_STREAM_PLAYBACK;
> + dir <= SNDRV_PCM_STREAM_CAPTURE; dir++) {
> + substream = pcm->streams[dir].substream;
> + if (!substream)
> + continue;
> +
> + buf = &substream->dma_buffer;
> + buf->area = dma_alloc_coherent(pcm->card->dev, PCM_BUF_BYTES,
> + &buf->addr, GFP_KERNEL);
> + buf->bytes = PCM_BUF_BYTES;
> + if (!buf->area)
> + return -ENOMEM;
> + }
> +
> + return 0;
> +}
> +
> +static void xlnx_logicpd_pcm_free(struct snd_pcm *pcm)
> +{
> + struct snd_pcm_substream *substream;
> + struct snd_dma_buffer *buf;
> + int dir;
> +
> + for (dir = SNDRV_PCM_STREAM_PLAYBACK;
> + dir <= SNDRV_PCM_STREAM_CAPTURE; dir++) {
> + substream = pcm->streams[dir].substream;
> + if (!substream)
> + continue;
> +
> + buf = &substream->dma_buffer;
> + if (!buf->area)
> + continue;
> +
> + dma_free_coherent(pcm->card->dev, buf->bytes,
> + buf->area, buf->addr);
> + buf->area = NULL;
> + }
> +}
> +
> +static const struct snd_soc_component_driver xlnx_logicpd_pcm_component = {
> + .name = "xlnx-logicp-pcm",
> + .ops = &xlnx_logicpd_pcm_ops,
> + .pcm_new = xlnx_logicpd_pcm_new,
> + .pcm_free = xlnx_logicpd_pcm_free,
> +};
> +
> +/* DAI methods */
> +
> +static void xlnx_logicpd_dai_int_en(struct xlnx_logicpd_i2s *i2s, int dir)
> +{
> + u32 reg;
> +
> + reg = readl_relaxed(i2s->base + INTR_MASK_REG(dir));
> + reg &= ~XFER_DONE_INTR;
> + writel(reg, i2s->base + INTR_MASK_REG(dir));
> +}
> +
> +static void xlnx_logicpd_dai_int_dis(struct xlnx_logicpd_i2s *i2s, int dir)
> +{
> + u32 reg;
> +
> + reg = readl_relaxed(i2s->base + INTR_MASK_REG(dir));
> + reg |= XFER_DONE_INTR;
> + writel_relaxed(reg, i2s->base + INTR_MASK_REG(dir));
> +}
> +
> +static irqreturn_t xlnx_logicpd_dai_isr(int irq, void *dev_id)
> +{
> + struct xlnx_logicpd_stream *stream = dev_id;
> + struct xlnx_logicpd_i2s *i2s = stream->i2s;
> + struct snd_pcm_substream *substream = stream->substream;
> + unsigned int period_sz = snd_pcm_lib_period_bytes(substream);
> + unsigned int buf_sz = snd_pcm_lib_buffer_bytes(substream);
> + dma_addr_t buf_addr = substream->dma_buffer.addr;
> + unsigned int dir = substream->stream;
> + u32 reg;
> +
> + /* Reading INTR_STAT deasserts the host interrupt */
> + reg = readl_relaxed(i2s->base + INTR_STAT_REG(dir));
> +
> + /*
> + * When the XFER_DONE interrupt is triggered, it means the period has
> + * been entirely shifted into the FIFO. At this point, we can move the
> + * buffer pointer to the next period and ask to transfer another chunk
> + * of data. Whenever the FIFO will be at its "almost full" state (4096
> + * words minus the threshold of 100 words) the internal DMA engine will
> + * automatically restart shifting data to the FIFO until its full state.
> + * Hence, the host has up to 3996 words (in our case, 3996 frames) to
> + * serve the interrupt before an underrun that would happen, at eg.
> + * 44100Hz, after 90ms.
> + */
> + if (reg & XFER_DONE_INTR) {
> + unsigned int offset_in_buf = ++stream->period_idx * period_sz;
> +
> + if (offset_in_buf >= buf_sz) {
> + stream->period_idx = 0;
> + offset_in_buf = stream->period_idx * period_sz;
> + }
> +
> + /* Move on to the next period in the overall buffer */
> + writel_relaxed(buf_addr + offset_in_buf,
> + i2s->base + BUFF_BASE_ADDR_REG(dir));
> + /* The last period might be smaller, update length if needed */
> + period_sz = min(period_sz, buf_sz - offset_in_buf);
> + writel_relaxed(BYTES_TO_WORDS(period_sz),
> + i2s->base + BUFF_LEN_REG(dir));
> +
> + /* Inform the PCM middle-layer */
> + snd_pcm_period_elapsed(substream);
> + }
> +
> + return IRQ_HANDLED;
> +}
> +
> +static int xlnx_logicpd_dai_trigger(struct snd_pcm_substream *substream,
> + int cmd, struct snd_soc_dai *dai)
> +{
> + struct xlnx_logicpd_i2s *i2s = snd_soc_dai_get_drvdata(dai);
> + unsigned int period_sz = snd_pcm_lib_period_bytes(substream);
> + dma_addr_t buf_addr = substream->dma_buffer.addr;
> + unsigned int dir = substream->stream;
> +
> + switch (cmd) {
> + case SNDRV_PCM_TRIGGER_START:
> + i2s->streams[dir].period_idx = 0;
> + /* Disable the other engine if enabled */
> + if (readl(i2s->base + CONTROL_REG(!dir)) & ENGINE_EN)
> + writel(0, i2s->base + CONTROL_REG(!dir));
> + /* Enable the desired engine */
> + writel_relaxed(ENGINE_EN, i2s->base + CONTROL_REG(dir));
> + /* Set the buffer start address */
> + writel_relaxed(buf_addr, i2s->base + BUFF_BASE_ADDR_REG(dir));
> + /* Enable the XFER_DONE IRQ, signaling the end of the period */
> + xlnx_logicpd_dai_int_en(i2s, dir);
> + /* Actually start the internal DMA engine */
> + writel(BYTES_TO_WORDS(period_sz),
> + i2s->base + BUFF_LEN_REG(dir));
> + break;
> + case SNDRV_PCM_TRIGGER_STOP:
> + /* Disable the interrupts */
> + xlnx_logicpd_dai_int_dis(i2s, dir);
> + /* Ensure the host IRQ is deasserted */
> + readl_relaxed(i2s->base + INTR_STAT_REG(dir));
> + break;
> + case SNDRV_PCM_TRIGGER_RESUME:
> + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
> + case SNDRV_PCM_TRIGGER_SUSPEND:
> + case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
> + default:
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static const struct snd_soc_dai_ops xlnx_logicpd_dai_ops = {
> + .trigger = xlnx_logicpd_dai_trigger,
> +};
> +
> +static int xlnx_logicpd_dai_probe(struct snd_soc_dai *dai)
> +{
> + struct xlnx_logicpd_i2s *i2s = snd_soc_dai_get_drvdata(dai);
> + unsigned int dir;
> +
> + for (dir = SNDRV_PCM_STREAM_PLAYBACK;
> + dir <= SNDRV_PCM_STREAM_CAPTURE; dir++) {
> + i2s->streams[dir].i2s = i2s;
> +
> + /* Reset the transmitter/receiver engine */
> + writel_relaxed(0, i2s->base + CONTROL_REG(dir));
> + /* Mask all interrupts */
> + writel_relaxed(GENMASK(31, 0), i2s->base + INTR_MASK_REG(dir));
> + }
> +
> + return 0;
> +}
> +
> +struct snd_soc_dai_driver xlnx_logicpd_dai = {
> + .name = "xylinx-logicpd-dai",
> + .probe = xlnx_logicpd_dai_probe,
> + .capture = {
> + .stream_name = "Capture",
> + .channels_min = 2,
> + .channels_max = 2,
> + .rates = SNDRV_PCM_RATE_8000_192000,
> + .rate_min = 8000,
> + .rate_max = 192000,
> + .formats = SNDRV_PCM_FMTBIT_S16_LE,
> + },
> + .playback = {
> + .stream_name = "Playback",
> + .channels_min = 2,
> + .channels_max = 2,
> + .rates = SNDRV_PCM_RATE_8000_192000,
> + .rate_min = 8000,
> + .rate_max = 192000,
> + .formats = SNDRV_PCM_FMTBIT_S16_LE,
> + },
> + .symmetric_rates = 1,
> + .ops = &xlnx_logicpd_dai_ops,
> +};
> +
> +static const struct snd_soc_component_driver xlnx_logicpd_i2s_component = {
> + .name = DRV_NAME,
> + .ops = &xlnx_logicpd_pcm_ops,
> +};
> +
> +static const struct of_device_id xlnx_logicpd_i2s_of_match[] = {
> + {
> + .compatible = "xlnx,logicpd-i2s-dai",
> + },
> + {},
> +};
> +MODULE_DEVICE_TABLE(of, xlnx_logicpd_i2s_of_match);
> +
> +static int xlnx_logicpd_i2s_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct xlnx_logicpd_i2s *i2s;
> + struct xlnx_logicpd_stream *stream;
> + int tx_irq, rx_irq, ret;
> + u32 reg;
> +
> + i2s = devm_kzalloc(dev, sizeof(*i2s), GFP_KERNEL);
> + if (!i2s)
> + return -ENOMEM;
> +
> + dev_set_drvdata(dev, i2s);
> +
> + i2s->base = devm_platform_ioremap_resource(pdev, 0);
> + if (IS_ERR(i2s->base))
> + return PTR_ERR(i2s->base);
> +
> + stream = &i2s->streams[SNDRV_PCM_STREAM_PLAYBACK];
> + tx_irq = platform_get_irq_byname(pdev, "tx");
> + if (tx_irq > 0) {
> + ret = devm_request_irq(dev, tx_irq, xlnx_logicpd_dai_isr,
> + 0, "logicpd-i2s-tx", stream);
> + if (ret)
> + return ret;
> + } else {
> + dev_err(dev, "TX IRQ not available (%d), disabling playback\n",
> + tx_irq);
> + tx_irq = 0;
> + }
> +
> + stream = &i2s->streams[SNDRV_PCM_STREAM_CAPTURE];
> + rx_irq = platform_get_irq_byname(pdev, "rx");
> + if (rx_irq > 0) {
> + ret = devm_request_irq(dev, rx_irq, xlnx_logicpd_dai_isr,
> + 0, "logicpd-i2s-rx", stream);
> + if (ret)
> + return ret;
> + } else {
> + dev_err(dev, "RX IRQ not available (%d), disabling capture\n",
> + rx_irq);
> + rx_irq = 0;
> + }
> +
> + if (!tx_irq && !rx_irq)
> + return -EINVAL;
> +
> + ret = devm_snd_soc_register_component(dev, &xlnx_logicpd_pcm_component,
> + NULL, 0);
> + if (ret) {
> + dev_err(dev, "cannot register PCM component (%d)\n", ret);
> + return ret;
> + }
> +
> + ret = devm_snd_soc_register_component(dev, &xlnx_logicpd_i2s_component,
> + &xlnx_logicpd_dai, 1);
> + if (ret) {
> + dev_err(dev, "cannot register I2S component (%d)\n", ret);
> + return ret;
> + }
> +
> + reg = readl_relaxed(i2s->base + IP_VERSION);
> + dev_info(dev, "%s DAI version %u.%u.%c (license: %s) registered\n",
> + xlnx_logicpd_dai.name,
> + (unsigned int)MAJOR_REV(reg),
> + (unsigned int)MINOR_REV(reg),
> + (char)PATCH_LEVEL(reg),
> + LICENSE_TYPE(reg) == 0 ? "source" :
> + (LICENSE_TYPE(reg) == 1 ? "eval" : "release"));
> +
> + return ret;
> +}
> +
> +static struct platform_driver xlnx_logicpd_i2s_driver = {
> + .driver = {
> + .name = DRV_NAME,
> + .of_match_table = xlnx_logicpd_i2s_of_match,
> + },
> + .probe = xlnx_logicpd_i2s_probe,
> +};
> +
> +module_platform_driver(xlnx_logicpd_i2s_driver);
> +
> +MODULE_AUTHOR("Miquel Raynal <miquel.raynal at bootlin.com>");
> +MODULE_DESCRIPTION("Xilinx logicPD I2S module");
> +MODULE_LICENSE("GPL v2");
>
More information about the Alsa-devel
mailing list