[1/9] drm: bridge/dw_hdmi-ahb-audio: add audio driver
diff mbox

Message ID E1ZO6hC-0003M5-EU@rmk-PC.arm.linux.org.uk
State New
Headers show

Commit Message

Russell King Aug. 8, 2015, 4:10 p.m. UTC
Add ALSA based HDMI AHB audio driver for dw_hdmi.  The only buffer
format supported by the hardware is its own special IEC958 based format,
which is not compatible with any ALSA format.  To avoid doing too much
data manipulation within the driver, we support only ALSAs IEC958 LE and
24-bit PCM formats for 2 to 6 channels, which we convert to its hardware
format.

A more desirable solution would be to have this conversion in userspace,
but ALSA does not appear to allow such transformations outside of
libasound itself.

Signed-off-by: Russell King <rmk+kernel@arm.linux.org.uk>
---
 drivers/gpu/drm/bridge/Kconfig             |  10 +
 drivers/gpu/drm/bridge/Makefile            |   1 +
 drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 561 +++++++++++++++++++++++++++++
 drivers/gpu/drm/bridge/dw_hdmi-audio.h     |  13 +
 drivers/gpu/drm/bridge/dw_hdmi.c           |  24 ++
 drivers/gpu/drm/bridge/dw_hdmi.h           |   3 +
 6 files changed, 612 insertions(+)
 create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
 create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-audio.h

Comments

Takashi Iwai Aug. 10, 2015, 10:05 a.m. UTC | #1
On Sat, 08 Aug 2015 18:10:06 +0200,
Russell King wrote:
> 
> Add ALSA based HDMI AHB audio driver for dw_hdmi.  The only buffer
> format supported by the hardware is its own special IEC958 based format,
> which is not compatible with any ALSA format.  To avoid doing too much
> data manipulation within the driver, we support only ALSAs IEC958 LE and
> 24-bit PCM formats for 2 to 6 channels, which we convert to its hardware
> format.
> 
> A more desirable solution would be to have this conversion in userspace,
> but ALSA does not appear to allow such transformations outside of
> libasound itself.
> 
> Signed-off-by: Russell King <rmk+kernel@arm.linux.org.uk>
> ---
>  drivers/gpu/drm/bridge/Kconfig             |  10 +
>  drivers/gpu/drm/bridge/Makefile            |   1 +
>  drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c | 561 +++++++++++++++++++++++++++++
>  drivers/gpu/drm/bridge/dw_hdmi-audio.h     |  13 +
>  drivers/gpu/drm/bridge/dw_hdmi.c           |  24 ++
>  drivers/gpu/drm/bridge/dw_hdmi.h           |   3 +
>  6 files changed, 612 insertions(+)
>  create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
>  create mode 100644 drivers/gpu/drm/bridge/dw_hdmi-audio.h
> 
> diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
> index acef3223772c..56ed35fe0734 100644
> --- a/drivers/gpu/drm/bridge/Kconfig
> +++ b/drivers/gpu/drm/bridge/Kconfig
> @@ -3,6 +3,16 @@ config DRM_DW_HDMI
>  	depends on DRM
>  	select DRM_KMS_HELPER
>  
> +config DRM_DW_HDMI_AHB_AUDIO
> +	tristate "Synopsis Designware AHB Audio interface"
> +	depends on DRM_DW_HDMI && SND
> +	select SND_PCM
> +	select SND_PCM_IEC958
> +	help
> +	  Support the AHB Audio interface which is part of the Synopsis
> +	  Designware HDMI block.  This is used in conjunction with
> +	  the i.MX6 HDMI driver.
> +
>  config DRM_PTN3460
>  	tristate "PTN3460 DP/LVDS bridge"
>  	depends on DRM
> diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
> index 8dfebd984370..eb80dbbb8365 100644
> --- a/drivers/gpu/drm/bridge/Makefile
> +++ b/drivers/gpu/drm/bridge/Makefile
> @@ -3,3 +3,4 @@ ccflags-y := -Iinclude/drm
>  obj-$(CONFIG_DRM_PS8622) += ps8622.o
>  obj-$(CONFIG_DRM_PTN3460) += ptn3460.o
>  obj-$(CONFIG_DRM_DW_HDMI) += dw_hdmi.o
> +obj-$(CONFIG_DRM_DW_HDMI_AHB_AUDIO) += dw_hdmi-ahb-audio.o
> diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
> new file mode 100644
> index 000000000000..22bbbc5c2393
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
> @@ -0,0 +1,561 @@
> +/*
> + * DesignWare HDMI audio driver
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License version 2 as
> + * published by the Free Software Foundation.
> + *
> + * Written and tested against the Designware HDMI Tx found in iMX6.
> + */
> +#include <linux/io.h>
> +#include <linux/interrupt.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <drm/bridge/dw_hdmi.h>
> +
> +#include <sound/asoundef.h>
> +#include <sound/core.h>
> +#include <sound/initval.h>
> +#include <sound/pcm.h>
> +#include <sound/pcm_iec958.h>
> +
> +#include "dw_hdmi-audio.h"
> +
> +#define DRIVER_NAME "dw-hdmi-ahb-audio"
> +
> +/* Provide some bits rather than bit offsets */
> +enum {
> +	HDMI_AHB_DMA_CONF0_SW_FIFO_RST = BIT(7),
> +	HDMI_AHB_DMA_CONF0_EN_HLOCK = BIT(3),
> +	HDMI_AHB_DMA_START_START = BIT(0),
> +	HDMI_AHB_DMA_STOP_STOP = BIT(0),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR = BIT(5),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST = BIT(4),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY = BIT(3),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE = BIT(2),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL = BIT(1),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0),
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL =
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR |
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST |
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY |
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE |
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL |
> +		HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY,
> +	HDMI_IH_AHBDMAAUD_STAT0_ERROR = BIT(5),
> +	HDMI_IH_AHBDMAAUD_STAT0_LOST = BIT(4),
> +	HDMI_IH_AHBDMAAUD_STAT0_RETRY = BIT(3),
> +	HDMI_IH_AHBDMAAUD_STAT0_DONE = BIT(2),
> +	HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL = BIT(1),
> +	HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0),
> +	HDMI_IH_AHBDMAAUD_STAT0_ALL =
> +		HDMI_IH_AHBDMAAUD_STAT0_ERROR |
> +		HDMI_IH_AHBDMAAUD_STAT0_LOST |
> +		HDMI_IH_AHBDMAAUD_STAT0_RETRY |
> +		HDMI_IH_AHBDMAAUD_STAT0_DONE |
> +		HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL |
> +		HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY,
> +	HDMI_AHB_DMA_CONF0_INCR16 = 2 << 1,
> +	HDMI_AHB_DMA_CONF0_INCR8 = 1 << 1,
> +	HDMI_AHB_DMA_CONF0_INCR4 = 0,
> +	HDMI_AHB_DMA_CONF0_BURST_MODE = BIT(0),
> +	HDMI_AHB_DMA_MASK_DONE = BIT(7),
> +	HDMI_REVISION_ID = 0x0001,
> +	HDMI_IH_AHBDMAAUD_STAT0 = 0x0109,
> +	HDMI_IH_MUTE_AHBDMAAUD_STAT0 = 0x0189,
> +	HDMI_AHB_DMA_CONF0 = 0x3600,
> +	HDMI_AHB_DMA_START = 0x3601,
> +	HDMI_AHB_DMA_STOP = 0x3602,
> +	HDMI_AHB_DMA_THRSLD = 0x3603,
> +	HDMI_AHB_DMA_STRADDR0 = 0x3604,
> +	HDMI_AHB_DMA_STPADDR0 = 0x3608,
> +	HDMI_AHB_DMA_MASK = 0x3614,
> +	HDMI_AHB_DMA_POL = 0x3615,
> +	HDMI_AHB_DMA_CONF1 = 0x3616,
> +	HDMI_AHB_DMA_BUFFPOL = 0x361a,
> +};
> +
> +struct snd_dw_hdmi {
> +	struct snd_card *card;
> +	struct snd_pcm *pcm;
> +	struct dw_hdmi_audio_data data;
> +	struct snd_pcm_substream *substream;
> +	void (*reformat)(struct snd_dw_hdmi *, size_t, size_t);
> +	void *buf_src;
> +	void *buf_dst;
> +	dma_addr_t buf_addr;
> +	unsigned buf_offset;
> +	unsigned buf_period;
> +	unsigned buf_size;
> +	unsigned channels;
> +	u8 revision;
> +	u8 iec_offset;
> +	u8 cs[192][8];
> +};
> +
> +static void dw_hdmi_writel(unsigned long val, void __iomem *ptr)

Better to be u32 instead of unsigned long in general.

> +{
> +	writeb_relaxed(val, ptr);
> +	writeb_relaxed(val >> 8, ptr + 1);
> +	writeb_relaxed(val >> 16, ptr + 2);
> +	writeb_relaxed(val >> 24, ptr + 3);
> +}
> +
> +/*
> + * Convert to hardware format: The userspace buffer contains IEC958 samples,
> + * with the PCUV bits in bits 31..28 and audio samples in bits 27..4.  We
> + * need these to be in bits 27..24, with the IEC B bit in bit 28, and audio
> + * samples in 23..0.
> + *
> + * Default preamble in bits 3..0: 8 = block start, 4 = even 2 = odd
> + *
> + * Ideally, we could do with having the data properly formatted in userspace.
> + */
> +static void dw_hdmi_reformat_iec958(struct snd_dw_hdmi *dw,
> +	size_t offset, size_t bytes)
> +{
> +	u32 *src = dw->buf_src + offset;
> +	u32 *dst = dw->buf_dst + offset;
> +	u32 *end = dw->buf_src + offset + bytes;
> +
> +	do {
> +		u32 b, sample = *src++;
> +
> +		b = (sample & 8) << (28 - 3);
> +
> +		sample >>= 4;
> +
> +		*dst++ = sample | b;
> +	} while (src < end);
> +}
> +
> +static u32 parity(u32 sample)
> +{
> +	sample ^= sample >> 16;
> +	sample ^= sample >> 8;
> +	sample ^= sample >> 4;
> +	sample ^= sample >> 2;
> +	sample ^= sample >> 1;
> +	return (sample & 1) << 27;
> +}
> +
> +static void dw_hdmi_reformat_s24(struct snd_dw_hdmi *dw,
> +	size_t offset, size_t bytes)
> +{
> +	u32 *src = dw->buf_src + offset;
> +	u32 *dst = dw->buf_dst + offset;
> +	u32 *end = dw->buf_src + offset + bytes;
> +
> +	do {
> +		unsigned i;
> +		u8 *cs;
> +
> +		cs = dw->cs[dw->iec_offset++];
> +		if (dw->iec_offset >= 192)
> +			dw->iec_offset = 0;
> +
> +		i = dw->channels;
> +		do {
> +			u32 sample = *src++;
> +
> +			sample &= ~0xff000000;
> +			sample |= *cs++ << 24;
> +			sample |= parity(sample & ~0xf8000000);
> +
> +			*dst++ = sample;
> +		} while (--i);
> +	} while (src < end);
> +}
> +
> +static void dw_hdmi_create_cs(struct snd_dw_hdmi *dw,
> +	struct snd_pcm_runtime *runtime)
> +{
> +	u8 cs[4];
> +	unsigned ch, i, j;
> +
> +	snd_pcm_create_iec958_consumer(runtime, cs, sizeof(cs));
> +
> +	memset(dw->cs, 0, sizeof(dw->cs));
> +
> +	for (ch = 0; ch < 8; ch++) {
> +		cs[2] &= ~IEC958_AES2_CON_CHANNEL;
> +		cs[2] |= (ch + 1) << 4;
> +
> +		for (i = 0; i < ARRAY_SIZE(cs); i++) {
> +			unsigned c = cs[i];
> +
> +			for (j = 0; j < 8; j++, c >>= 1)
> +				dw->cs[i * 8 + j][ch] = (c & 1) << 2;
> +		}
> +	}
> +	dw->cs[0][0] |= BIT(4);
> +}
> +
> +static void dw_hdmi_start_dma(struct snd_dw_hdmi *dw)
> +{
> +	void __iomem *base = dw->data.base;
> +	unsigned offset = dw->buf_offset;
> +	unsigned period = dw->buf_period;
> +	u32 start, stop;
> +
> +	dw->reformat(dw, offset, period);
> +
> +	/* Clear all irqs before enabling irqs and starting DMA */
> +	writeb_relaxed(HDMI_IH_AHBDMAAUD_STAT0_ALL,
> +		       base + HDMI_IH_AHBDMAAUD_STAT0);
> +
> +	start = dw->buf_addr + offset;
> +	stop = start + period - 1;
> +
> +	/* Setup the hardware start/stop addresses */
> +	dw_hdmi_writel(start, base + HDMI_AHB_DMA_STRADDR0);
> +	dw_hdmi_writel(stop, base + HDMI_AHB_DMA_STPADDR0);
> +
> +	writeb_relaxed((u8)~HDMI_AHB_DMA_MASK_DONE, base + HDMI_AHB_DMA_MASK);
> +	writeb(HDMI_AHB_DMA_START_START, base + HDMI_AHB_DMA_START);
> +
> +	offset += period;
> +	if (offset >= dw->buf_size)
> +		offset = 0;
> +	dw->buf_offset = offset;
> +}
> +
> +static void dw_hdmi_stop_dma(struct snd_dw_hdmi *dw)
> +{
> +	dw->substream = NULL;
> +
> +	/* Disable interrupts before disabling DMA */
> +	writeb_relaxed(~0, dw->data.base + HDMI_AHB_DMA_MASK);
> +	writeb_relaxed(HDMI_AHB_DMA_STOP_STOP, dw->data.base + HDMI_AHB_DMA_STOP);
> +}
> +
> +static irqreturn_t snd_dw_hdmi_irq(int irq, void *data)
> +{
> +	struct snd_dw_hdmi *dw = data;
> +	struct snd_pcm_substream *substream;
> +	unsigned stat;
> +
> +	stat = readb_relaxed(dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> +	if (!stat)
> +		return IRQ_NONE;
> +
> +	writeb_relaxed(stat, dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> +
> +	substream = dw->substream;
> +	if (stat & HDMI_IH_AHBDMAAUD_STAT0_DONE && substream) {
> +		snd_pcm_period_elapsed(substream);
> +		if (dw->substream)
> +			dw_hdmi_start_dma(dw);
> +	}

Don't we need locking?  In theory, the trigger can be issued while the
irq is being handled.

> +
> +	return IRQ_HANDLED;
> +}
> +
> +static struct snd_pcm_hardware dw_hdmi_hw = {
> +	.info = SNDRV_PCM_INFO_INTERLEAVED |
> +		SNDRV_PCM_INFO_BLOCK_TRANSFER |
> +		SNDRV_PCM_INFO_MMAP |
> +		SNDRV_PCM_INFO_MMAP_VALID,
> +	.formats = SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE |
> +		   SNDRV_PCM_FMTBIT_S24_LE,
> +	.rates = SNDRV_PCM_RATE_32000 |
> +		 SNDRV_PCM_RATE_44100 |
> +		 SNDRV_PCM_RATE_48000 |
> +		 SNDRV_PCM_RATE_88200 |
> +		 SNDRV_PCM_RATE_96000 |
> +		 SNDRV_PCM_RATE_176400 |
> +		 SNDRV_PCM_RATE_192000,
> +	.channels_min = 2,
> +	.channels_max = 8,
> +	.buffer_bytes_max = 64 * 1024,
> +	.period_bytes_min = 256,
> +	.period_bytes_max = 8192,	/* ERR004323: must limit to 8k */
> +	.periods_min = 2,
> +	.periods_max = 16,
> +	.fifo_size = 0,
> +};
> +
> +static int dw_hdmi_open(struct snd_pcm_substream *substream)
> +{
> +	struct snd_pcm_runtime *runtime = substream->runtime;
> +	struct snd_dw_hdmi *dw = substream->private_data;
> +	void __iomem *base = dw->data.base;
> +	int ret;
> +
> +	runtime->hw = dw_hdmi_hw;
> +
> +	ret = snd_pcm_limit_hw_rates(runtime);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
> +	if (ret < 0)
> +		return ret;
> +
> +	/* Clear FIFO */
> +	writeb_relaxed(HDMI_AHB_DMA_CONF0_SW_FIFO_RST,
> +		       base + HDMI_AHB_DMA_CONF0);
> +
> +	/* Configure interrupt polarities */
> +	writeb_relaxed(~0, base + HDMI_AHB_DMA_POL);
> +	writeb_relaxed(~0, base + HDMI_AHB_DMA_BUFFPOL);
> +
> +	/* Keep interrupts masked, and clear any pending */
> +	writeb_relaxed(~0, base + HDMI_AHB_DMA_MASK);
> +	writeb_relaxed(~0, base + HDMI_IH_AHBDMAAUD_STAT0);
> +
> +	ret = request_irq(dw->data.irq, snd_dw_hdmi_irq, IRQF_SHARED,
> +			  "dw-hdmi-audio", dw);
> +	if (ret)
> +		return ret;
> +
> +	/* Un-mute done interrupt */
> +	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL &
> +		       ~HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE,
> +		       base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
> +
> +	return 0;
> +}
> +
> +static int dw_hdmi_close(struct snd_pcm_substream *substream)
> +{
> +	struct snd_dw_hdmi *dw = substream->private_data;
> +
> +	/* Mute all interrupts */
> +	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL,
> +		       dw->data.base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
> +
> +	free_irq(dw->data.irq, dw);
> +
> +	return 0;
> +}
> +
> +static int dw_hdmi_hw_free(struct snd_pcm_substream *substream)
> +{
> +	return snd_pcm_lib_free_vmalloc_buffer(substream);
> +}
> +
> +static int dw_hdmi_hw_params(struct snd_pcm_substream *substream,
> +	struct snd_pcm_hw_params *params)
> +{
> +	return snd_pcm_lib_alloc_vmalloc_buffer(substream,
> +						params_buffer_bytes(params));
> +}
> +
> +static int dw_hdmi_prepare(struct snd_pcm_substream *substream)
> +{
> +	struct snd_pcm_runtime *runtime = substream->runtime;
> +	struct snd_dw_hdmi *dw = substream->private_data;
> +	u8 threshold, conf0, conf1;
> +
> +	/* Setup as per 3.0.5 FSL 4.1.0 BSP */
> +	switch (dw->revision) {
> +	case 0x0a:
> +		conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE |
> +			HDMI_AHB_DMA_CONF0_INCR4;
> +		if (runtime->channels == 2)
> +			threshold = 126;
> +		else
> +			threshold = 124;
> +		break;
> +	case 0x1a:
> +		conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE |
> +			HDMI_AHB_DMA_CONF0_INCR8;
> +		threshold = 128;
> +		break;
> +	default:
> +		/* NOTREACHED */
> +		return -EINVAL;
> +	}
> +
> +	dw_hdmi_set_sample_rate(dw->data.hdmi, runtime->rate);
> +
> +	/* Minimum number of bytes in the fifo. */
> +	runtime->hw.fifo_size = threshold * 32;
> +
> +	conf0 |= HDMI_AHB_DMA_CONF0_EN_HLOCK;
> +	conf1 = (1 << runtime->channels) - 1;
> +
> +	writeb_relaxed(threshold, dw->data.base + HDMI_AHB_DMA_THRSLD);
> +	writeb_relaxed(conf0, dw->data.base + HDMI_AHB_DMA_CONF0);
> +	writeb_relaxed(conf1, dw->data.base + HDMI_AHB_DMA_CONF1);
> +
> +	switch (runtime->format) {
> +	case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE:
> +		dw->reformat = dw_hdmi_reformat_iec958;
> +		break;
> +	case SNDRV_PCM_FORMAT_S24_LE:
> +		dw_hdmi_create_cs(dw, runtime);
> +		dw->reformat = dw_hdmi_reformat_s24;
> +		break;
> +	}
> +	dw->iec_offset = 0;
> +	dw->channels = runtime->channels;
> +	dw->buf_src  = runtime->dma_area;
> +	dw->buf_dst  = substream->dma_buffer.area;
> +	dw->buf_addr = substream->dma_buffer.addr;
> +	dw->buf_period = snd_pcm_lib_period_bytes(substream);
> +	dw->buf_size = snd_pcm_lib_buffer_bytes(substream);
> +
> +	return 0;
> +}
> +
> +static int dw_hdmi_trigger(struct snd_pcm_substream *substream, int cmd)
> +{
> +	struct snd_dw_hdmi *dw = substream->private_data;
> +	int ret = 0;
> +
> +	switch (cmd) {
> +	case SNDRV_PCM_TRIGGER_START:
> +		dw->buf_offset = 0;
> +		dw->substream = substream;
> +		dw_hdmi_start_dma(dw);
> +		dw_hdmi_audio_enable(dw->data.hdmi);
> +		substream->runtime->delay = substream->runtime->period_size;
> +		break;
> +
> +	case SNDRV_PCM_TRIGGER_STOP:
> +		dw_hdmi_stop_dma(dw);
> +		dw_hdmi_audio_disable(dw->data.hdmi);
> +		break;
> +
> +	default:
> +		ret = -EINVAL;
> +		break;

SNDRV_PCM_TRIGGER_SUSPEND may be passed at suspend, too.


> +	}
> +
> +	return ret;
> +}
> +
> +static snd_pcm_uframes_t dw_hdmi_pointer(struct snd_pcm_substream *substream)
> +{
> +	struct snd_pcm_runtime *runtime = substream->runtime;
> +	struct snd_dw_hdmi *dw = substream->private_data;
> +
> +	return bytes_to_frames(runtime, dw->buf_offset);

So, this returns the offset that has been reformatted.  Does the
hardware support any better position reporting?  We may give the delay
from the driver if possible.


thanks,

Takashi


> +}
> +
> +static struct snd_pcm_ops snd_dw_hdmi_ops = {
> +	.open = dw_hdmi_open,
> +	.close = dw_hdmi_close,
> +	.ioctl = snd_pcm_lib_ioctl,
> +	.hw_params = dw_hdmi_hw_params,
> +	.hw_free = dw_hdmi_hw_free,
> +	.prepare = dw_hdmi_prepare,
> +	.trigger = dw_hdmi_trigger,
> +	.pointer = dw_hdmi_pointer,
> +	.page = snd_pcm_lib_get_vmalloc_page,
> +};
> +
> +static int snd_dw_hdmi_probe(struct platform_device *pdev)
> +{
> +	const struct dw_hdmi_audio_data *data = pdev->dev.platform_data;
> +	struct device *dev = pdev->dev.parent;
> +	struct snd_dw_hdmi *dw;
> +	struct snd_card *card;
> +	struct snd_pcm *pcm;
> +	unsigned revision;
> +	int ret;
> +
> +	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL,
> +		       data->base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
> +	revision = readb_relaxed(data->base + HDMI_REVISION_ID);
> +	if (revision != 0x0a && revision != 0x1a) {
> +		dev_err(dev, "dw-hdmi-audio: unknown revision 0x%02x\n",
> +			revision);
> +		return -ENXIO;
> +	}
> +
> +	ret = snd_card_new(dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
> +			      THIS_MODULE, sizeof(struct snd_dw_hdmi), &card);
> +	if (ret < 0)
> +		return ret;
> +
> +	strlcpy(card->driver, DRIVER_NAME, sizeof(card->driver));
> +	strlcpy(card->shortname, "DW-HDMI", sizeof(card->shortname));
> +	snprintf(card->longname, sizeof(card->longname),
> +		 "%s rev 0x%02x, irq %d", card->shortname, revision,
> +		 data->irq);
> +
> +	dw = card->private_data;
> +	dw->card = card;
> +	dw->data = *data;
> +	dw->revision = revision;
> +
> +	ret = snd_pcm_new(card, "DW HDMI", 0, 1, 0, &pcm);
> +	if (ret < 0)
> +		goto err;
> +
> +	dw->pcm = pcm;
> +	pcm->private_data = dw;
> +	strlcpy(pcm->name, DRIVER_NAME, sizeof(pcm->name));
> +	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_dw_hdmi_ops);
> +
> +	snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
> +			dev, 64 * 1024, 64 * 1024);
> +
> +	ret = snd_card_register(card);
> +	if (ret < 0)
> +		goto err;
> +
> +	platform_set_drvdata(pdev, dw);
> +
> +	return 0;
> +
> +err:
> +	snd_card_free(card);
> +	return ret;
> +}
> +
> +static int snd_dw_hdmi_remove(struct platform_device *pdev)
> +{
> +	struct snd_dw_hdmi *dw = platform_get_drvdata(pdev);
> +
> +	snd_card_free(dw->card);
> +
> +	return 0;
> +}
> +
> +#ifdef CONFIG_PM_SLEEP
> +static int snd_dw_hdmi_suspend(struct device *dev)
> +{
> +	struct snd_dw_hdmi *dw = dev_get_drvdata(dev);
> +
> +	snd_power_change_state(dw->card, SNDRV_CTL_POWER_D3cold);
> +	snd_pcm_suspend_all(dw->pcm);
> +
> +	return 0;
> +}
> +
> +static int snd_dw_hdmi_resume(struct device *dev)
> +{
> +	struct snd_dw_hdmi *dw = dev_get_drvdata(dev);
> +
> +	snd_power_change_state(dw->card, SNDRV_CTL_POWER_D0);
> +
> +	return 0;
> +}
> +
> +static SIMPLE_DEV_PM_OPS(snd_dw_hdmi_pm, snd_dw_hdmi_suspend,
> +			 snd_dw_hdmi_resume);
> +#define PM_OPS &snd_dw_hdmi_pm
> +#else
> +#define PM_OPS NULL
> +#endif
> +
> +static struct platform_driver snd_dw_hdmi_driver = {
> +	.probe	= snd_dw_hdmi_probe,
> +	.remove	= snd_dw_hdmi_remove,
> +	.driver	= {
> +		.name = DRIVER_NAME,
> +		.owner = THIS_MODULE,
> +		.pm = PM_OPS,
> +	},
> +};
> +
> +module_platform_driver(snd_dw_hdmi_driver);
> +
> +MODULE_AUTHOR("Russell King <rmk+kernel@arm.linux.org.uk>");
> +MODULE_DESCRIPTION("Synopsis Designware HDMI AHB ALSA interface");
> +MODULE_LICENSE("GPL v2");
> +MODULE_ALIAS("platform:" DRIVER_NAME);
> diff --git a/drivers/gpu/drm/bridge/dw_hdmi-audio.h b/drivers/gpu/drm/bridge/dw_hdmi-audio.h
> new file mode 100644
> index 000000000000..1e840118d90a
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/dw_hdmi-audio.h
> @@ -0,0 +1,13 @@
> +#ifndef DW_HDMI_AUDIO_H
> +#define DW_HDMI_AUDIO_H
> +
> +struct dw_hdmi;
> +
> +struct dw_hdmi_audio_data {
> +	phys_addr_t phys;
> +	void __iomem *base;
> +	int irq;
> +	struct dw_hdmi *hdmi;
> +};
> +
> +#endif
> diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c
> index fba25607ef88..b65464789fbd 100644
> --- a/drivers/gpu/drm/bridge/dw_hdmi.c
> +++ b/drivers/gpu/drm/bridge/dw_hdmi.c
> @@ -28,6 +28,7 @@
>  #include <drm/bridge/dw_hdmi.h>
>  
>  #include "dw_hdmi.h"
> +#include "dw_hdmi-audio.h"
>  
>  #define HDMI_EDID_LEN		512
>  
> @@ -104,6 +105,7 @@ struct dw_hdmi {
>  	struct drm_encoder *encoder;
>  	struct drm_bridge *bridge;
>  
> +	struct platform_device *audio;
>  	enum dw_hdmi_devtype dev_type;
>  	struct device *dev;
>  	struct clk *isfr_clk;
> @@ -1732,7 +1734,9 @@ int dw_hdmi_bind(struct device *dev, struct device *master,
>  {
>  	struct drm_device *drm = data;
>  	struct device_node *np = dev->of_node;
> +	struct platform_device_info pdevinfo;
>  	struct device_node *ddc_node;
> +	struct dw_hdmi_audio_data audio;
>  	struct dw_hdmi *hdmi;
>  	int ret;
>  	u32 val = 1;
> @@ -1860,6 +1864,23 @@ int dw_hdmi_bind(struct device *dev, struct device *master,
>  	hdmi_writeb(hdmi, ~(HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE),
>  		    HDMI_IH_MUTE_PHY_STAT0);
>  
> +	memset(&pdevinfo, 0, sizeof(pdevinfo));
> +	pdevinfo.parent = dev;
> +	pdevinfo.id = PLATFORM_DEVID_AUTO;
> +
> +	if (hdmi_readb(hdmi, HDMI_CONFIG1_ID) & HDMI_CONFIG1_AHB) {
> +		audio.phys = iores->start;
> +		audio.base = hdmi->regs;
> +		audio.irq = irq;
> +		audio.hdmi = hdmi;
> +
> +		pdevinfo.name = "dw-hdmi-ahb-audio";
> +		pdevinfo.data = &audio;
> +		pdevinfo.size_data = sizeof(audio);
> +		pdevinfo.dma_mask = DMA_BIT_MASK(32);
> +		hdmi->audio = platform_device_register_full(&pdevinfo);
> +	}
> +
>  	dev_set_drvdata(dev, hdmi);
>  
>  	return 0;
> @@ -1877,6 +1898,9 @@ void dw_hdmi_unbind(struct device *dev, struct device *master, void *data)
>  {
>  	struct dw_hdmi *hdmi = dev_get_drvdata(dev);
>  
> +	if (hdmi->audio && !IS_ERR(hdmi->audio))
> +		platform_device_unregister(hdmi->audio);
> +
>  	/* Disable all interrupts */
>  	hdmi_writeb(hdmi, ~0, HDMI_IH_MUTE_PHY_STAT0);
>  
> diff --git a/drivers/gpu/drm/bridge/dw_hdmi.h b/drivers/gpu/drm/bridge/dw_hdmi.h
> index 175dbc89a824..78e54e813212 100644
> --- a/drivers/gpu/drm/bridge/dw_hdmi.h
> +++ b/drivers/gpu/drm/bridge/dw_hdmi.h
> @@ -545,6 +545,9 @@
>  #define HDMI_I2CM_FS_SCL_LCNT_0_ADDR            0x7E12
>  
>  enum {
> +/* CONFIG1_ID field values */
> +	HDMI_CONFIG1_AHB = 0x01,
> +
>  /* IH_FC_INT2 field values */
>  	HDMI_IH_FC_INT2_OVERFLOW_MASK = 0x03,
>  	HDMI_IH_FC_INT2_LOW_PRIORITY_OVERFLOW = 0x02,
> -- 
> 2.1.0
>
Russell King - ARM Linux Aug. 10, 2015, 10:39 a.m. UTC | #2
On Mon, Aug 10, 2015 at 12:05:07PM +0200, Takashi Iwai wrote:
> On Sat, 08 Aug 2015 18:10:06 +0200,
> Russell King wrote:
> > +static irqreturn_t snd_dw_hdmi_irq(int irq, void *data)
> > +{
> > +	struct snd_dw_hdmi *dw = data;
> > +	struct snd_pcm_substream *substream;
> > +	unsigned stat;
> > +
> > +	stat = readb_relaxed(dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> > +	if (!stat)
> > +		return IRQ_NONE;
> > +
> > +	writeb_relaxed(stat, dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> > +
> > +	substream = dw->substream;
> > +	if (stat & HDMI_IH_AHBDMAAUD_STAT0_DONE && substream) {
> > +		snd_pcm_period_elapsed(substream);
> > +		if (dw->substream)
> > +			dw_hdmi_start_dma(dw);
> > +	}
> 
> Don't we need locking?

Possibly.

> In theory, the trigger can be issued while the irq is being handled.

Well, we can't have a lock around the whole of the above, because that
results in deadlock (as snd_pcm_period_elapsed() can end up calling into
the trigger method.)  I'm not happy to throw a spinlock around this
because of the in-built format conversion (something else I'm really not
happy about - which has to exist here because alsalib is soo painful
to add custom sample reformatting to - such modules have to be built
as part of alsalib itself rather than an add-on module.)

> > +static int dw_hdmi_trigger(struct snd_pcm_substream *substream, int cmd)
> > +{
> > +	struct snd_dw_hdmi *dw = substream->private_data;
> > +	int ret = 0;
> > +
> > +	switch (cmd) {
> > +	case SNDRV_PCM_TRIGGER_START:
> > +		dw->buf_offset = 0;
> > +		dw->substream = substream;
> > +		dw_hdmi_start_dma(dw);
> > +		dw_hdmi_audio_enable(dw->data.hdmi);
> > +		substream->runtime->delay = substream->runtime->period_size;
> > +		break;
> > +
> > +	case SNDRV_PCM_TRIGGER_STOP:
> > +		dw_hdmi_stop_dma(dw);
> > +		dw_hdmi_audio_disable(dw->data.hdmi);
> > +		break;
> > +
> > +	default:
> > +		ret = -EINVAL;
> > +		break;
> 
> SNDRV_PCM_TRIGGER_SUSPEND may be passed at suspend, too.

I think rather than adding code which would be difficult for me to test,
I'd instead remove the suspend/resume callbacks, or at least disable them
until someone can test that feature, or is willing to implement it.

> > +static snd_pcm_uframes_t dw_hdmi_pointer(struct snd_pcm_substream *substream)
> > +{
> > +	struct snd_pcm_runtime *runtime = substream->runtime;
> > +	struct snd_dw_hdmi *dw = substream->private_data;
> > +
> > +	return bytes_to_frames(runtime, dw->buf_offset);
> 
> So, this returns the offset that has been reformatted.  Does the
> hardware support any better position reporting?  We may give the delay
> from the driver if possible.

Basically, no.  Reading a 32-bit DMA position as separate bytes while
DMA is active is racy.

This is the best we can do, and the way we report the position has been
arrived at after what's getting on for two years of testing with
pulseaudio, vlc direct access & spdif pass-through, aplay, etc:

Author: Russell King <rmk+kernel@arm.linux.org.uk>
Date:   Thu Nov 7 16:01:45 2013 +0000

    drm: bridge/dw_hdmi-ahb-audio: add audio driver
Takashi Iwai Aug. 10, 2015, 12:23 p.m. UTC | #3
On Mon, 10 Aug 2015 12:39:21 +0200,
Russell King - ARM Linux wrote:
> 
> On Mon, Aug 10, 2015 at 12:05:07PM +0200, Takashi Iwai wrote:
> > On Sat, 08 Aug 2015 18:10:06 +0200,
> > Russell King wrote:
> > > +static irqreturn_t snd_dw_hdmi_irq(int irq, void *data)
> > > +{
> > > +	struct snd_dw_hdmi *dw = data;
> > > +	struct snd_pcm_substream *substream;
> > > +	unsigned stat;
> > > +
> > > +	stat = readb_relaxed(dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> > > +	if (!stat)
> > > +		return IRQ_NONE;
> > > +
> > > +	writeb_relaxed(stat, dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
> > > +
> > > +	substream = dw->substream;
> > > +	if (stat & HDMI_IH_AHBDMAAUD_STAT0_DONE && substream) {
> > > +		snd_pcm_period_elapsed(substream);
> > > +		if (dw->substream)
> > > +			dw_hdmi_start_dma(dw);
> > > +	}
> > 
> > Don't we need locking?
> 
> Possibly.
> 
> > In theory, the trigger can be issued while the irq is being handled.
> 
> Well, we can't have a lock around the whole of the above, because that
> results in deadlock (as snd_pcm_period_elapsed() can end up calling into
> the trigger method.)

Yes, and a usual workaround is to unlock temporarily at calling
snd_pcm_period_elapsed(), then relock or call it at the end of 
handler.

> I'm not happy to throw a spinlock around this
> because of the in-built format conversion (something else I'm really not
> happy about - which has to exist here because alsalib is soo painful
> to add custom sample reformatting to - such modules have to be built
> as part of alsalib itself rather than an add-on module.)

I admit that alsa-lib code is very horrible to follow -- but I guess
the change you'd need for iec958 plugin would be fairly small.  We can
add a config option and let iec958 behaving slightly differently
depending on it.

Meanwhile, having an in-kernel workaround makes it much easier to
deploy, so I think it's OK to have this in driver for now.

> > > +static int dw_hdmi_trigger(struct snd_pcm_substream *substream, int cmd)
> > > +{
> > > +	struct snd_dw_hdmi *dw = substream->private_data;
> > > +	int ret = 0;
> > > +
> > > +	switch (cmd) {
> > > +	case SNDRV_PCM_TRIGGER_START:
> > > +		dw->buf_offset = 0;
> > > +		dw->substream = substream;
> > > +		dw_hdmi_start_dma(dw);
> > > +		dw_hdmi_audio_enable(dw->data.hdmi);
> > > +		substream->runtime->delay = substream->runtime->period_size;
> > > +		break;
> > > +
> > > +	case SNDRV_PCM_TRIGGER_STOP:
> > > +		dw_hdmi_stop_dma(dw);
> > > +		dw_hdmi_audio_disable(dw->data.hdmi);
> > > +		break;
> > > +
> > > +	default:
> > > +		ret = -EINVAL;
> > > +		break;
> > 
> > SNDRV_PCM_TRIGGER_SUSPEND may be passed at suspend, too.
> 
> I think rather than adding code which would be difficult for me to test,
> I'd instead remove the suspend/resume callbacks, or at least disable them
> until someone can test that feature, or is willing to implement it.

That's fine.

> > > +static snd_pcm_uframes_t dw_hdmi_pointer(struct snd_pcm_substream *substream)
> > > +{
> > > +	struct snd_pcm_runtime *runtime = substream->runtime;
> > > +	struct snd_dw_hdmi *dw = substream->private_data;
> > > +
> > > +	return bytes_to_frames(runtime, dw->buf_offset);
> > 
> > So, this returns the offset that has been reformatted.  Does the
> > hardware support any better position reporting?  We may give the delay
> > from the driver if possible.
> 
> Basically, no.  Reading a 32-bit DMA position as separate bytes while
> DMA is active is racy.
> 
> This is the best we can do, and the way we report the position has been
> arrived at after what's getting on for two years of testing with
> pulseaudio, vlc direct access & spdif pass-through, aplay, etc:
> 
> Author: Russell King <rmk+kernel@arm.linux.org.uk>
> Date:   Thu Nov 7 16:01:45 2013 +0000
> 
>     drm: bridge/dw_hdmi-ahb-audio: add audio driver

OK, then this is a driver with the low update granularity.  Hopefully
we'll get some good API to indicate that in near future, as we've been
discussing about it for a while.


thanks,

Takashi
Russell King - ARM Linux Aug. 10, 2015, 4:49 p.m. UTC | #4
On Mon, Aug 10, 2015 at 02:23:07PM +0200, Takashi Iwai wrote:
> I admit that alsa-lib code is very horrible to follow -- but I guess
> the change you'd need for iec958 plugin would be fairly small.  We can
> add a config option and let iec958 behaving slightly differently
> depending on it.

Yes, but there's other problems there as well.

The IEC958 plugin does the job of adding the 4 AES bytes and formatting
fairly well, but the problem when the 'default' bytes specified in the
ALSA configuration files are used.

Let's take the old chestnut of PulseAudio, or even aplay, or the miriad
of other audio-only players out there.

Most of them do not supply the AES bytes to be used, so we end up with
the default.

The default is... 0x04 0x82 0x00 0x02, which specifies a sample rate
of 48kHz.  However, the actual sample rate may not be 48kHz.  At least
the HDMI specifications say that the channel status data must be correct,
and there are AV receivers out there which do make use of this, and if
the channel status does not agree with the actual sample rate, they
either refuse to recognise the audio stream (saying there's nothing
there) or they intermittently mute the audio.  Yamaha RX-V677 is one
example which has this behaviour.

The only compliant program that I've found so far is VLC in SPDIF
pass-through mode, which is the only case where VLC passes the
channel status information.  Everything else seems broken in this
regard, by falling back to the default.

Obviously, aplay can be made to work by setting the AES bytes
manually when specifying the device for it to use, but this is not
really user-friendly or programmer friendly - especially as the
current use model expects things to "just work" (the common case
being PCM output on a PC which doesn't care about channel status.)

I'm not sure what the right solution is here: modifying every audio
player out there to make HDMI work sanely is crazy.  Having alsalib
automatically generate the correct AES channel status bytes for
linear audio formats seems to be sensible, but difficult given its
present structure with the defaults - the iec958 plugin has no idea
if the defaults are being used or not.

The advantage of having the horrid conversion in the kernel is that
we can choose to generate proper AES channel status data without
regard to userspace for standard linear PCM, and when the iec958 plugin
is being used with proper channel status (eg, in compressed audio
pass-through mode by VLC) then that works too.
Mark Brown Aug. 10, 2015, 6:16 p.m. UTC | #5
On Mon, Aug 10, 2015 at 05:49:41PM +0100, Russell King - ARM Linux wrote:

> I'm not sure what the right solution is here: modifying every audio
> player out there to make HDMI work sanely is crazy.  Having alsalib
> automatically generate the correct AES channel status bytes for
> linear audio formats seems to be sensible, but difficult given its
> present structure with the defaults - the iec958 plugin has no idea
> if the defaults are being used or not.

> The advantage of having the horrid conversion in the kernel is that
> we can choose to generate proper AES channel status data without
> regard to userspace for standard linear PCM, and when the iec958 plugin
> is being used with proper channel status (eg, in compressed audio
> pass-through mode by VLC) then that works too.

The other advantage of doing it in kernel is that it also fixes tinyalsa
applications (which mainly means Android systems) by default for PCM
data.
Fabio Estevam Oct. 6, 2015, 6:07 p.m. UTC | #6
On Sat, Aug 8, 2015 at 1:10 PM, Russell King
<rmk+kernel@arm.linux.org.uk> wrote:
> Add ALSA based HDMI AHB audio driver for dw_hdmi.  The only buffer
> format supported by the hardware is its own special IEC958 based format,
> which is not compatible with any ALSA format.  To avoid doing too much
> data manipulation within the driver, we support only ALSAs IEC958 LE and
> 24-bit PCM formats for 2 to 6 channels, which we convert to its hardware
> format.
>
> A more desirable solution would be to have this conversion in userspace,
> but ALSA does not appear to allow such transformations outside of
> libasound itself.
>
> Signed-off-by: Russell King <rmk+kernel@arm.linux.org.uk>

I applied this series, but the HDMI audio card is not registered. I
guess I missed the dts pieces. Are the dts patches available?

Thanks
Russell King - ARM Linux Oct. 6, 2015, 6:18 p.m. UTC | #7
On Tue, Oct 06, 2015 at 03:07:40PM -0300, Fabio Estevam wrote:
> On Sat, Aug 8, 2015 at 1:10 PM, Russell King
> <rmk+kernel@arm.linux.org.uk> wrote:
> > Add ALSA based HDMI AHB audio driver for dw_hdmi.  The only buffer
> > format supported by the hardware is its own special IEC958 based format,
> > which is not compatible with any ALSA format.  To avoid doing too much
> > data manipulation within the driver, we support only ALSAs IEC958 LE and
> > 24-bit PCM formats for 2 to 6 channels, which we convert to its hardware
> > format.
> >
> > A more desirable solution would be to have this conversion in userspace,
> > but ALSA does not appear to allow such transformations outside of
> > libasound itself.
> >
> > Signed-off-by: Russell King <rmk+kernel@arm.linux.org.uk>
> 
> I applied this series, but the HDMI audio card is not registered. I
> guess I missed the dts pieces. Are the dts patches available?

Sorry, I've been out for most of the day.  There's no DT patches required.

The dw_hdmi bridge driver creates its own platform device for the audio,
which should then bind to the dw_hdmi-ahb-audio driver using normal Linux
methods.

I don't know what's wrong with your setup, for me, it just works:

[    1.358829] dwhdmi-imx 120000.hdmi: Detected HDMI controller 0x13:0xa:0xa0:0xc1
[    1.377173] imx-drm display-subsystem: bound 120000.hdmi (ops dw_hdmi_imx_ops)
...
[    2.851343] ALSA device list:
[    2.857364]   #0: DW-HDMI rev 0x0a, irq 21

as it always has done for me.  There's nothing special about it.

There's nothing special about it - it's a standard ALSA PCM audio driver,
it doesn't have any requirements over and above having ALSA and ALSA's PCM
support enabled.  If you didn't have those enabled, but somehow had the
dw_hdmi-ahb-audio set as built-in, you'd get a link time error, so it's
not a configuration issue.

It works for me on all iMX6 platforms (solo, dual-lite, dual and quad).

As it's a straight ALSA device, it doesn't need any codec or any of the
ASoC infrastructure either.

I guess the thing to start looking at is whether the device and driver
appear in /sys/bus/platform/{devices,drivers}/.  You should have:

/sys/bus/platform/devices/dw-hdmi-ahb-audio.0.auto 
/sys/bus/platform/drivers/dw-hdmi-ahb-audio

and obviously the latter should contain the symlink to the device.  If
that is present, then the driver has bound, and it should appear in
/proc/asound/cards.
Fabio Estevam Oct. 6, 2015, 6:45 p.m. UTC | #8
On Tue, Oct 6, 2015 at 3:18 PM, Russell King - ARM Linux
<linux@arm.linux.org.uk> wrote:

> Sorry, I've been out for most of the day.  There's no DT patches required.
>
> The dw_hdmi bridge driver creates its own platform device for the audio,
> which should then bind to the dw_hdmi-ahb-audio driver using normal Linux
> methods.
>
> I don't know what's wrong with your setup, for me, it just works:
>
> [    1.358829] dwhdmi-imx 120000.hdmi: Detected HDMI controller 0x13:0xa:0xa0:0xc1
> [    1.377173] imx-drm display-subsystem: bound 120000.hdmi (ops dw_hdmi_imx_ops)
> ...
> [    2.851343] ALSA device list:
> [    2.857364]   #0: DW-HDMI rev 0x0a, irq 21
>
> as it always has done for me.  There's nothing special about it.

Great, got it to probe now:

[    7.454760] ALSA device list:
[    7.457764]   #0: DW-HDMI rev 0x0a, irq 19
[    7.461990]   #1: wm8962-audio

There was a conflict and I resolved incorrectly here. Will try to play
a wav file via aplay now.

Thanks
Russell King - ARM Linux Oct. 6, 2015, 6:54 p.m. UTC | #9
On Tue, Oct 06, 2015 at 03:45:32PM -0300, Fabio Estevam wrote:
> On Tue, Oct 6, 2015 at 3:18 PM, Russell King - ARM Linux
> <linux@arm.linux.org.uk> wrote:
> 
> > Sorry, I've been out for most of the day.  There's no DT patches required.
> >
> > The dw_hdmi bridge driver creates its own platform device for the audio,
> > which should then bind to the dw_hdmi-ahb-audio driver using normal Linux
> > methods.
> >
> > I don't know what's wrong with your setup, for me, it just works:
> >
> > [    1.358829] dwhdmi-imx 120000.hdmi: Detected HDMI controller 0x13:0xa:0xa0:0xc1
> > [    1.377173] imx-drm display-subsystem: bound 120000.hdmi (ops dw_hdmi_imx_ops)
> > ...
> > [    2.851343] ALSA device list:
> > [    2.857364]   #0: DW-HDMI rev 0x0a, irq 21
> >
> > as it always has done for me.  There's nothing special about it.
> 
> Great, got it to probe now:
> 
> [    7.454760] ALSA device list:
> [    7.457764]   #0: DW-HDMI rev 0x0a, irq 19
> [    7.461990]   #1: wm8962-audio
> 
> There was a conflict and I resolved incorrectly here. Will try to play
> a wav file via aplay now.

Make sure you have the ALSA config file, as alsalib won't get on
with dw-hdmi only accepting 24-bit audio without this.  A copy is
attached.  It also tells ALSA how to deal with multi-channel audio
as well.
Fabio Estevam Oct. 6, 2015, 8:25 p.m. UTC | #10
On Tue, Oct 6, 2015 at 3:54 PM, Russell King - ARM Linux
<linux@arm.linux.org.uk> wrote:

> Make sure you have the ALSA config file, as alsalib won't get on
> with dw-hdmi only accepting 24-bit audio without this.  A copy is
> attached.  It also tells ALSA how to deal with multi-channel audio
> as well.

Thanks, Russell!

Got audio to play on my HDMI TV :-)

For the entire series:

Tested-by: Fabio Estevam <fabio.estevam@freescale.com>
Russell King - ARM Linux Oct. 9, 2015, 4 p.m. UTC | #11
On Tue, Oct 06, 2015 at 05:25:16PM -0300, Fabio Estevam wrote:
> On Tue, Oct 6, 2015 at 3:54 PM, Russell King - ARM Linux
> <linux@arm.linux.org.uk> wrote:
> 
> > Make sure you have the ALSA config file, as alsalib won't get on
> > with dw-hdmi only accepting 24-bit audio without this.  A copy is
> > attached.  It also tells ALSA how to deal with multi-channel audio
> > as well.
> 
> Thanks, Russell!
> 
> Got audio to play on my HDMI TV :-)
> 
> For the entire series:
> 
> Tested-by: Fabio Estevam <fabio.estevam@freescale.com>

Just to confirm - that's for _all_ of these 8 patches, including the
changes to the ACR code in the last four patches, and you're happy that
I send all of these:

drm: bridge/dw_hdmi-ahb-audio: add audio driver
drm: bridge/dw_hdmi-ahb-audio: parse ELD from HDMI driver
drm: bridge/dw_hdmi-ahb-audio: basic support for multi-channel PCM audio
drm: bridge/dw_hdmi-ahb-audio: allow larger buffer sizes
drm: bridge/dw_hdmi: avoid being recursive in N calculation
drm: bridge/dw_hdmi: adjust pixel clock values in N calculation
drm: bridge/dw_hdmi: remove ratio support from ACR code
drm: bridge/dw_hdmi: replace CTS calculation for the ACR
Fabio Estevam Oct. 9, 2015, 4:02 p.m. UTC | #12
On Fri, Oct 9, 2015 at 1:00 PM, Russell King - ARM Linux
<linux@arm.linux.org.uk> wrote:

>> Thanks, Russell!
>>
>> Got audio to play on my HDMI TV :-)
>>
>> For the entire series:
>>
>> Tested-by: Fabio Estevam <fabio.estevam@freescale.com>
>
> Just to confirm - that's for _all_ of these 8 patches, including the
> changes to the ACR code in the last four patches, and you're happy that
> I send all of these:
>
> drm: bridge/dw_hdmi-ahb-audio: add audio driver
> drm: bridge/dw_hdmi-ahb-audio: parse ELD from HDMI driver
> drm: bridge/dw_hdmi-ahb-audio: basic support for multi-channel PCM audio
> drm: bridge/dw_hdmi-ahb-audio: allow larger buffer sizes
> drm: bridge/dw_hdmi: avoid being recursive in N calculation
> drm: bridge/dw_hdmi: adjust pixel clock values in N calculation
> drm: bridge/dw_hdmi: remove ratio support from ACR code
> drm: bridge/dw_hdmi: replace CTS calculation for the ACR

That's correct. Thanks, Russell
Russell King - ARM Linux Oct. 9, 2015, 4:11 p.m. UTC | #13
On Fri, Oct 09, 2015 at 01:02:11PM -0300, Fabio Estevam wrote:
> On Fri, Oct 9, 2015 at 1:00 PM, Russell King - ARM Linux
> <linux@arm.linux.org.uk> wrote:
> 
> >> Thanks, Russell!
> >>
> >> Got audio to play on my HDMI TV :-)
> >>
> >> For the entire series:
> >>
> >> Tested-by: Fabio Estevam <fabio.estevam@freescale.com>
> >
> > Just to confirm - that's for _all_ of these 8 patches, including the
> > changes to the ACR code in the last four patches, and you're happy that
> > I send all of these:
> >
> > drm: bridge/dw_hdmi-ahb-audio: add audio driver
> > drm: bridge/dw_hdmi-ahb-audio: parse ELD from HDMI driver
> > drm: bridge/dw_hdmi-ahb-audio: basic support for multi-channel PCM audio
> > drm: bridge/dw_hdmi-ahb-audio: allow larger buffer sizes
> > drm: bridge/dw_hdmi: avoid being recursive in N calculation
> > drm: bridge/dw_hdmi: adjust pixel clock values in N calculation
> > drm: bridge/dw_hdmi: remove ratio support from ACR code
> > drm: bridge/dw_hdmi: replace CTS calculation for the ACR
> 
> That's correct. Thanks, Russell

Thanks.  I'll drop that set into linux-next tonight, along with the TDA998x
and Armada DRM patches that haven't seen an airing there yet - before asking
David to pull them next week (the timescale has slipped...)

Patch
diff mbox

diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
index acef3223772c..56ed35fe0734 100644
--- a/drivers/gpu/drm/bridge/Kconfig
+++ b/drivers/gpu/drm/bridge/Kconfig
@@ -3,6 +3,16 @@  config DRM_DW_HDMI
 	depends on DRM
 	select DRM_KMS_HELPER
 
+config DRM_DW_HDMI_AHB_AUDIO
+	tristate "Synopsis Designware AHB Audio interface"
+	depends on DRM_DW_HDMI && SND
+	select SND_PCM
+	select SND_PCM_IEC958
+	help
+	  Support the AHB Audio interface which is part of the Synopsis
+	  Designware HDMI block.  This is used in conjunction with
+	  the i.MX6 HDMI driver.
+
 config DRM_PTN3460
 	tristate "PTN3460 DP/LVDS bridge"
 	depends on DRM
diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
index 8dfebd984370..eb80dbbb8365 100644
--- a/drivers/gpu/drm/bridge/Makefile
+++ b/drivers/gpu/drm/bridge/Makefile
@@ -3,3 +3,4 @@  ccflags-y := -Iinclude/drm
 obj-$(CONFIG_DRM_PS8622) += ps8622.o
 obj-$(CONFIG_DRM_PTN3460) += ptn3460.o
 obj-$(CONFIG_DRM_DW_HDMI) += dw_hdmi.o
+obj-$(CONFIG_DRM_DW_HDMI_AHB_AUDIO) += dw_hdmi-ahb-audio.o
diff --git a/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
new file mode 100644
index 000000000000..22bbbc5c2393
--- /dev/null
+++ b/drivers/gpu/drm/bridge/dw_hdmi-ahb-audio.c
@@ -0,0 +1,561 @@ 
+/*
+ * DesignWare HDMI audio driver
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * Written and tested against the Designware HDMI Tx found in iMX6.
+ */
+#include <linux/io.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <drm/bridge/dw_hdmi.h>
+
+#include <sound/asoundef.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+#include <sound/pcm_iec958.h>
+
+#include "dw_hdmi-audio.h"
+
+#define DRIVER_NAME "dw-hdmi-ahb-audio"
+
+/* Provide some bits rather than bit offsets */
+enum {
+	HDMI_AHB_DMA_CONF0_SW_FIFO_RST = BIT(7),
+	HDMI_AHB_DMA_CONF0_EN_HLOCK = BIT(3),
+	HDMI_AHB_DMA_START_START = BIT(0),
+	HDMI_AHB_DMA_STOP_STOP = BIT(0),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR = BIT(5),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST = BIT(4),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY = BIT(3),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE = BIT(2),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL = BIT(1),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0),
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL =
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_ERROR |
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_LOST |
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_RETRY |
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE |
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFFULL |
+		HDMI_IH_MUTE_AHBDMAAUD_STAT0_BUFFEMPTY,
+	HDMI_IH_AHBDMAAUD_STAT0_ERROR = BIT(5),
+	HDMI_IH_AHBDMAAUD_STAT0_LOST = BIT(4),
+	HDMI_IH_AHBDMAAUD_STAT0_RETRY = BIT(3),
+	HDMI_IH_AHBDMAAUD_STAT0_DONE = BIT(2),
+	HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL = BIT(1),
+	HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY = BIT(0),
+	HDMI_IH_AHBDMAAUD_STAT0_ALL =
+		HDMI_IH_AHBDMAAUD_STAT0_ERROR |
+		HDMI_IH_AHBDMAAUD_STAT0_LOST |
+		HDMI_IH_AHBDMAAUD_STAT0_RETRY |
+		HDMI_IH_AHBDMAAUD_STAT0_DONE |
+		HDMI_IH_AHBDMAAUD_STAT0_BUFFFULL |
+		HDMI_IH_AHBDMAAUD_STAT0_BUFFEMPTY,
+	HDMI_AHB_DMA_CONF0_INCR16 = 2 << 1,
+	HDMI_AHB_DMA_CONF0_INCR8 = 1 << 1,
+	HDMI_AHB_DMA_CONF0_INCR4 = 0,
+	HDMI_AHB_DMA_CONF0_BURST_MODE = BIT(0),
+	HDMI_AHB_DMA_MASK_DONE = BIT(7),
+	HDMI_REVISION_ID = 0x0001,
+	HDMI_IH_AHBDMAAUD_STAT0 = 0x0109,
+	HDMI_IH_MUTE_AHBDMAAUD_STAT0 = 0x0189,
+	HDMI_AHB_DMA_CONF0 = 0x3600,
+	HDMI_AHB_DMA_START = 0x3601,
+	HDMI_AHB_DMA_STOP = 0x3602,
+	HDMI_AHB_DMA_THRSLD = 0x3603,
+	HDMI_AHB_DMA_STRADDR0 = 0x3604,
+	HDMI_AHB_DMA_STPADDR0 = 0x3608,
+	HDMI_AHB_DMA_MASK = 0x3614,
+	HDMI_AHB_DMA_POL = 0x3615,
+	HDMI_AHB_DMA_CONF1 = 0x3616,
+	HDMI_AHB_DMA_BUFFPOL = 0x361a,
+};
+
+struct snd_dw_hdmi {
+	struct snd_card *card;
+	struct snd_pcm *pcm;
+	struct dw_hdmi_audio_data data;
+	struct snd_pcm_substream *substream;
+	void (*reformat)(struct snd_dw_hdmi *, size_t, size_t);
+	void *buf_src;
+	void *buf_dst;
+	dma_addr_t buf_addr;
+	unsigned buf_offset;
+	unsigned buf_period;
+	unsigned buf_size;
+	unsigned channels;
+	u8 revision;
+	u8 iec_offset;
+	u8 cs[192][8];
+};
+
+static void dw_hdmi_writel(unsigned long val, void __iomem *ptr)
+{
+	writeb_relaxed(val, ptr);
+	writeb_relaxed(val >> 8, ptr + 1);
+	writeb_relaxed(val >> 16, ptr + 2);
+	writeb_relaxed(val >> 24, ptr + 3);
+}
+
+/*
+ * Convert to hardware format: The userspace buffer contains IEC958 samples,
+ * with the PCUV bits in bits 31..28 and audio samples in bits 27..4.  We
+ * need these to be in bits 27..24, with the IEC B bit in bit 28, and audio
+ * samples in 23..0.
+ *
+ * Default preamble in bits 3..0: 8 = block start, 4 = even 2 = odd
+ *
+ * Ideally, we could do with having the data properly formatted in userspace.
+ */
+static void dw_hdmi_reformat_iec958(struct snd_dw_hdmi *dw,
+	size_t offset, size_t bytes)
+{
+	u32 *src = dw->buf_src + offset;
+	u32 *dst = dw->buf_dst + offset;
+	u32 *end = dw->buf_src + offset + bytes;
+
+	do {
+		u32 b, sample = *src++;
+
+		b = (sample & 8) << (28 - 3);
+
+		sample >>= 4;
+
+		*dst++ = sample | b;
+	} while (src < end);
+}
+
+static u32 parity(u32 sample)
+{
+	sample ^= sample >> 16;
+	sample ^= sample >> 8;
+	sample ^= sample >> 4;
+	sample ^= sample >> 2;
+	sample ^= sample >> 1;
+	return (sample & 1) << 27;
+}
+
+static void dw_hdmi_reformat_s24(struct snd_dw_hdmi *dw,
+	size_t offset, size_t bytes)
+{
+	u32 *src = dw->buf_src + offset;
+	u32 *dst = dw->buf_dst + offset;
+	u32 *end = dw->buf_src + offset + bytes;
+
+	do {
+		unsigned i;
+		u8 *cs;
+
+		cs = dw->cs[dw->iec_offset++];
+		if (dw->iec_offset >= 192)
+			dw->iec_offset = 0;
+
+		i = dw->channels;
+		do {
+			u32 sample = *src++;
+
+			sample &= ~0xff000000;
+			sample |= *cs++ << 24;
+			sample |= parity(sample & ~0xf8000000);
+
+			*dst++ = sample;
+		} while (--i);
+	} while (src < end);
+}
+
+static void dw_hdmi_create_cs(struct snd_dw_hdmi *dw,
+	struct snd_pcm_runtime *runtime)
+{
+	u8 cs[4];
+	unsigned ch, i, j;
+
+	snd_pcm_create_iec958_consumer(runtime, cs, sizeof(cs));
+
+	memset(dw->cs, 0, sizeof(dw->cs));
+
+	for (ch = 0; ch < 8; ch++) {
+		cs[2] &= ~IEC958_AES2_CON_CHANNEL;
+		cs[2] |= (ch + 1) << 4;
+
+		for (i = 0; i < ARRAY_SIZE(cs); i++) {
+			unsigned c = cs[i];
+
+			for (j = 0; j < 8; j++, c >>= 1)
+				dw->cs[i * 8 + j][ch] = (c & 1) << 2;
+		}
+	}
+	dw->cs[0][0] |= BIT(4);
+}
+
+static void dw_hdmi_start_dma(struct snd_dw_hdmi *dw)
+{
+	void __iomem *base = dw->data.base;
+	unsigned offset = dw->buf_offset;
+	unsigned period = dw->buf_period;
+	u32 start, stop;
+
+	dw->reformat(dw, offset, period);
+
+	/* Clear all irqs before enabling irqs and starting DMA */
+	writeb_relaxed(HDMI_IH_AHBDMAAUD_STAT0_ALL,
+		       base + HDMI_IH_AHBDMAAUD_STAT0);
+
+	start = dw->buf_addr + offset;
+	stop = start + period - 1;
+
+	/* Setup the hardware start/stop addresses */
+	dw_hdmi_writel(start, base + HDMI_AHB_DMA_STRADDR0);
+	dw_hdmi_writel(stop, base + HDMI_AHB_DMA_STPADDR0);
+
+	writeb_relaxed((u8)~HDMI_AHB_DMA_MASK_DONE, base + HDMI_AHB_DMA_MASK);
+	writeb(HDMI_AHB_DMA_START_START, base + HDMI_AHB_DMA_START);
+
+	offset += period;
+	if (offset >= dw->buf_size)
+		offset = 0;
+	dw->buf_offset = offset;
+}
+
+static void dw_hdmi_stop_dma(struct snd_dw_hdmi *dw)
+{
+	dw->substream = NULL;
+
+	/* Disable interrupts before disabling DMA */
+	writeb_relaxed(~0, dw->data.base + HDMI_AHB_DMA_MASK);
+	writeb_relaxed(HDMI_AHB_DMA_STOP_STOP, dw->data.base + HDMI_AHB_DMA_STOP);
+}
+
+static irqreturn_t snd_dw_hdmi_irq(int irq, void *data)
+{
+	struct snd_dw_hdmi *dw = data;
+	struct snd_pcm_substream *substream;
+	unsigned stat;
+
+	stat = readb_relaxed(dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
+	if (!stat)
+		return IRQ_NONE;
+
+	writeb_relaxed(stat, dw->data.base + HDMI_IH_AHBDMAAUD_STAT0);
+
+	substream = dw->substream;
+	if (stat & HDMI_IH_AHBDMAAUD_STAT0_DONE && substream) {
+		snd_pcm_period_elapsed(substream);
+		if (dw->substream)
+			dw_hdmi_start_dma(dw);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static struct snd_pcm_hardware dw_hdmi_hw = {
+	.info = SNDRV_PCM_INFO_INTERLEAVED |
+		SNDRV_PCM_INFO_BLOCK_TRANSFER |
+		SNDRV_PCM_INFO_MMAP |
+		SNDRV_PCM_INFO_MMAP_VALID,
+	.formats = SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE |
+		   SNDRV_PCM_FMTBIT_S24_LE,
+	.rates = SNDRV_PCM_RATE_32000 |
+		 SNDRV_PCM_RATE_44100 |
+		 SNDRV_PCM_RATE_48000 |
+		 SNDRV_PCM_RATE_88200 |
+		 SNDRV_PCM_RATE_96000 |
+		 SNDRV_PCM_RATE_176400 |
+		 SNDRV_PCM_RATE_192000,
+	.channels_min = 2,
+	.channels_max = 8,
+	.buffer_bytes_max = 64 * 1024,
+	.period_bytes_min = 256,
+	.period_bytes_max = 8192,	/* ERR004323: must limit to 8k */
+	.periods_min = 2,
+	.periods_max = 16,
+	.fifo_size = 0,
+};
+
+static int dw_hdmi_open(struct snd_pcm_substream *substream)
+{
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	struct snd_dw_hdmi *dw = substream->private_data;
+	void __iomem *base = dw->data.base;
+	int ret;
+
+	runtime->hw = dw_hdmi_hw;
+
+	ret = snd_pcm_limit_hw_rates(runtime);
+	if (ret < 0)
+		return ret;
+
+	ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
+	if (ret < 0)
+		return ret;
+
+	/* Clear FIFO */
+	writeb_relaxed(HDMI_AHB_DMA_CONF0_SW_FIFO_RST,
+		       base + HDMI_AHB_DMA_CONF0);
+
+	/* Configure interrupt polarities */
+	writeb_relaxed(~0, base + HDMI_AHB_DMA_POL);
+	writeb_relaxed(~0, base + HDMI_AHB_DMA_BUFFPOL);
+
+	/* Keep interrupts masked, and clear any pending */
+	writeb_relaxed(~0, base + HDMI_AHB_DMA_MASK);
+	writeb_relaxed(~0, base + HDMI_IH_AHBDMAAUD_STAT0);
+
+	ret = request_irq(dw->data.irq, snd_dw_hdmi_irq, IRQF_SHARED,
+			  "dw-hdmi-audio", dw);
+	if (ret)
+		return ret;
+
+	/* Un-mute done interrupt */
+	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL &
+		       ~HDMI_IH_MUTE_AHBDMAAUD_STAT0_DONE,
+		       base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
+
+	return 0;
+}
+
+static int dw_hdmi_close(struct snd_pcm_substream *substream)
+{
+	struct snd_dw_hdmi *dw = substream->private_data;
+
+	/* Mute all interrupts */
+	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL,
+		       dw->data.base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
+
+	free_irq(dw->data.irq, dw);
+
+	return 0;
+}
+
+static int dw_hdmi_hw_free(struct snd_pcm_substream *substream)
+{
+	return snd_pcm_lib_free_vmalloc_buffer(substream);
+}
+
+static int dw_hdmi_hw_params(struct snd_pcm_substream *substream,
+	struct snd_pcm_hw_params *params)
+{
+	return snd_pcm_lib_alloc_vmalloc_buffer(substream,
+						params_buffer_bytes(params));
+}
+
+static int dw_hdmi_prepare(struct snd_pcm_substream *substream)
+{
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	struct snd_dw_hdmi *dw = substream->private_data;
+	u8 threshold, conf0, conf1;
+
+	/* Setup as per 3.0.5 FSL 4.1.0 BSP */
+	switch (dw->revision) {
+	case 0x0a:
+		conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE |
+			HDMI_AHB_DMA_CONF0_INCR4;
+		if (runtime->channels == 2)
+			threshold = 126;
+		else
+			threshold = 124;
+		break;
+	case 0x1a:
+		conf0 = HDMI_AHB_DMA_CONF0_BURST_MODE |
+			HDMI_AHB_DMA_CONF0_INCR8;
+		threshold = 128;
+		break;
+	default:
+		/* NOTREACHED */
+		return -EINVAL;
+	}
+
+	dw_hdmi_set_sample_rate(dw->data.hdmi, runtime->rate);
+
+	/* Minimum number of bytes in the fifo. */
+	runtime->hw.fifo_size = threshold * 32;
+
+	conf0 |= HDMI_AHB_DMA_CONF0_EN_HLOCK;
+	conf1 = (1 << runtime->channels) - 1;
+
+	writeb_relaxed(threshold, dw->data.base + HDMI_AHB_DMA_THRSLD);
+	writeb_relaxed(conf0, dw->data.base + HDMI_AHB_DMA_CONF0);
+	writeb_relaxed(conf1, dw->data.base + HDMI_AHB_DMA_CONF1);
+
+	switch (runtime->format) {
+	case SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE:
+		dw->reformat = dw_hdmi_reformat_iec958;
+		break;
+	case SNDRV_PCM_FORMAT_S24_LE:
+		dw_hdmi_create_cs(dw, runtime);
+		dw->reformat = dw_hdmi_reformat_s24;
+		break;
+	}
+	dw->iec_offset = 0;
+	dw->channels = runtime->channels;
+	dw->buf_src  = runtime->dma_area;
+	dw->buf_dst  = substream->dma_buffer.area;
+	dw->buf_addr = substream->dma_buffer.addr;
+	dw->buf_period = snd_pcm_lib_period_bytes(substream);
+	dw->buf_size = snd_pcm_lib_buffer_bytes(substream);
+
+	return 0;
+}
+
+static int dw_hdmi_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+	struct snd_dw_hdmi *dw = substream->private_data;
+	int ret = 0;
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+		dw->buf_offset = 0;
+		dw->substream = substream;
+		dw_hdmi_start_dma(dw);
+		dw_hdmi_audio_enable(dw->data.hdmi);
+		substream->runtime->delay = substream->runtime->period_size;
+		break;
+
+	case SNDRV_PCM_TRIGGER_STOP:
+		dw_hdmi_stop_dma(dw);
+		dw_hdmi_audio_disable(dw->data.hdmi);
+		break;
+
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
+static snd_pcm_uframes_t dw_hdmi_pointer(struct snd_pcm_substream *substream)
+{
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	struct snd_dw_hdmi *dw = substream->private_data;
+
+	return bytes_to_frames(runtime, dw->buf_offset);
+}
+
+static struct snd_pcm_ops snd_dw_hdmi_ops = {
+	.open = dw_hdmi_open,
+	.close = dw_hdmi_close,
+	.ioctl = snd_pcm_lib_ioctl,
+	.hw_params = dw_hdmi_hw_params,
+	.hw_free = dw_hdmi_hw_free,
+	.prepare = dw_hdmi_prepare,
+	.trigger = dw_hdmi_trigger,
+	.pointer = dw_hdmi_pointer,
+	.page = snd_pcm_lib_get_vmalloc_page,
+};
+
+static int snd_dw_hdmi_probe(struct platform_device *pdev)
+{
+	const struct dw_hdmi_audio_data *data = pdev->dev.platform_data;
+	struct device *dev = pdev->dev.parent;
+	struct snd_dw_hdmi *dw;
+	struct snd_card *card;
+	struct snd_pcm *pcm;
+	unsigned revision;
+	int ret;
+
+	writeb_relaxed(HDMI_IH_MUTE_AHBDMAAUD_STAT0_ALL,
+		       data->base + HDMI_IH_MUTE_AHBDMAAUD_STAT0);
+	revision = readb_relaxed(data->base + HDMI_REVISION_ID);
+	if (revision != 0x0a && revision != 0x1a) {
+		dev_err(dev, "dw-hdmi-audio: unknown revision 0x%02x\n",
+			revision);
+		return -ENXIO;
+	}
+
+	ret = snd_card_new(dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
+			      THIS_MODULE, sizeof(struct snd_dw_hdmi), &card);
+	if (ret < 0)
+		return ret;
+
+	strlcpy(card->driver, DRIVER_NAME, sizeof(card->driver));
+	strlcpy(card->shortname, "DW-HDMI", sizeof(card->shortname));
+	snprintf(card->longname, sizeof(card->longname),
+		 "%s rev 0x%02x, irq %d", card->shortname, revision,
+		 data->irq);
+
+	dw = card->private_data;
+	dw->card = card;
+	dw->data = *data;
+	dw->revision = revision;
+
+	ret = snd_pcm_new(card, "DW HDMI", 0, 1, 0, &pcm);
+	if (ret < 0)
+		goto err;
+
+	dw->pcm = pcm;
+	pcm->private_data = dw;
+	strlcpy(pcm->name, DRIVER_NAME, sizeof(pcm->name));
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_dw_hdmi_ops);
+
+	snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
+			dev, 64 * 1024, 64 * 1024);
+
+	ret = snd_card_register(card);
+	if (ret < 0)
+		goto err;
+
+	platform_set_drvdata(pdev, dw);
+
+	return 0;
+
+err:
+	snd_card_free(card);
+	return ret;
+}
+
+static int snd_dw_hdmi_remove(struct platform_device *pdev)
+{
+	struct snd_dw_hdmi *dw = platform_get_drvdata(pdev);
+
+	snd_card_free(dw->card);
+
+	return 0;
+}
+
+#ifdef CONFIG_PM_SLEEP
+static int snd_dw_hdmi_suspend(struct device *dev)
+{
+	struct snd_dw_hdmi *dw = dev_get_drvdata(dev);
+
+	snd_power_change_state(dw->card, SNDRV_CTL_POWER_D3cold);
+	snd_pcm_suspend_all(dw->pcm);
+
+	return 0;
+}
+
+static int snd_dw_hdmi_resume(struct device *dev)
+{
+	struct snd_dw_hdmi *dw = dev_get_drvdata(dev);
+
+	snd_power_change_state(dw->card, SNDRV_CTL_POWER_D0);
+
+	return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(snd_dw_hdmi_pm, snd_dw_hdmi_suspend,
+			 snd_dw_hdmi_resume);
+#define PM_OPS &snd_dw_hdmi_pm
+#else
+#define PM_OPS NULL
+#endif
+
+static struct platform_driver snd_dw_hdmi_driver = {
+	.probe	= snd_dw_hdmi_probe,
+	.remove	= snd_dw_hdmi_remove,
+	.driver	= {
+		.name = DRIVER_NAME,
+		.owner = THIS_MODULE,
+		.pm = PM_OPS,
+	},
+};
+
+module_platform_driver(snd_dw_hdmi_driver);
+
+MODULE_AUTHOR("Russell King <rmk+kernel@arm.linux.org.uk>");
+MODULE_DESCRIPTION("Synopsis Designware HDMI AHB ALSA interface");
+MODULE_LICENSE("GPL v2");
+MODULE_ALIAS("platform:" DRIVER_NAME);
diff --git a/drivers/gpu/drm/bridge/dw_hdmi-audio.h b/drivers/gpu/drm/bridge/dw_hdmi-audio.h
new file mode 100644
index 000000000000..1e840118d90a
--- /dev/null
+++ b/drivers/gpu/drm/bridge/dw_hdmi-audio.h
@@ -0,0 +1,13 @@ 
+#ifndef DW_HDMI_AUDIO_H
+#define DW_HDMI_AUDIO_H
+
+struct dw_hdmi;
+
+struct dw_hdmi_audio_data {
+	phys_addr_t phys;
+	void __iomem *base;
+	int irq;
+	struct dw_hdmi *hdmi;
+};
+
+#endif
diff --git a/drivers/gpu/drm/bridge/dw_hdmi.c b/drivers/gpu/drm/bridge/dw_hdmi.c
index fba25607ef88..b65464789fbd 100644
--- a/drivers/gpu/drm/bridge/dw_hdmi.c
+++ b/drivers/gpu/drm/bridge/dw_hdmi.c
@@ -28,6 +28,7 @@ 
 #include <drm/bridge/dw_hdmi.h>
 
 #include "dw_hdmi.h"
+#include "dw_hdmi-audio.h"
 
 #define HDMI_EDID_LEN		512
 
@@ -104,6 +105,7 @@  struct dw_hdmi {
 	struct drm_encoder *encoder;
 	struct drm_bridge *bridge;
 
+	struct platform_device *audio;
 	enum dw_hdmi_devtype dev_type;
 	struct device *dev;
 	struct clk *isfr_clk;
@@ -1732,7 +1734,9 @@  int dw_hdmi_bind(struct device *dev, struct device *master,
 {
 	struct drm_device *drm = data;
 	struct device_node *np = dev->of_node;
+	struct platform_device_info pdevinfo;
 	struct device_node *ddc_node;
+	struct dw_hdmi_audio_data audio;
 	struct dw_hdmi *hdmi;
 	int ret;
 	u32 val = 1;
@@ -1860,6 +1864,23 @@  int dw_hdmi_bind(struct device *dev, struct device *master,
 	hdmi_writeb(hdmi, ~(HDMI_IH_PHY_STAT0_HPD | HDMI_IH_PHY_STAT0_RX_SENSE),
 		    HDMI_IH_MUTE_PHY_STAT0);
 
+	memset(&pdevinfo, 0, sizeof(pdevinfo));
+	pdevinfo.parent = dev;
+	pdevinfo.id = PLATFORM_DEVID_AUTO;
+
+	if (hdmi_readb(hdmi, HDMI_CONFIG1_ID) & HDMI_CONFIG1_AHB) {
+		audio.phys = iores->start;
+		audio.base = hdmi->regs;
+		audio.irq = irq;
+		audio.hdmi = hdmi;
+
+		pdevinfo.name = "dw-hdmi-ahb-audio";
+		pdevinfo.data = &audio;
+		pdevinfo.size_data = sizeof(audio);
+		pdevinfo.dma_mask = DMA_BIT_MASK(32);
+		hdmi->audio = platform_device_register_full(&pdevinfo);
+	}
+
 	dev_set_drvdata(dev, hdmi);
 
 	return 0;
@@ -1877,6 +1898,9 @@  void dw_hdmi_unbind(struct device *dev, struct device *master, void *data)
 {
 	struct dw_hdmi *hdmi = dev_get_drvdata(dev);
 
+	if (hdmi->audio && !IS_ERR(hdmi->audio))
+		platform_device_unregister(hdmi->audio);
+
 	/* Disable all interrupts */
 	hdmi_writeb(hdmi, ~0, HDMI_IH_MUTE_PHY_STAT0);
 
diff --git a/drivers/gpu/drm/bridge/dw_hdmi.h b/drivers/gpu/drm/bridge/dw_hdmi.h
index 175dbc89a824..78e54e813212 100644
--- a/drivers/gpu/drm/bridge/dw_hdmi.h
+++ b/drivers/gpu/drm/bridge/dw_hdmi.h
@@ -545,6 +545,9 @@ 
 #define HDMI_I2CM_FS_SCL_LCNT_0_ADDR            0x7E12
 
 enum {
+/* CONFIG1_ID field values */
+	HDMI_CONFIG1_AHB = 0x01,
+
 /* IH_FC_INT2 field values */
 	HDMI_IH_FC_INT2_OVERFLOW_MASK = 0x03,
 	HDMI_IH_FC_INT2_LOW_PRIORITY_OVERFLOW = 0x02,