ASoC: blackfin: bf5xx-i2s: Add support for TDM mode

The bf5xx-i2s{,-pcm} and bf5xx-tdm{-pcm} drivers are nearly identical. Both are
for the same hardware each supporting a slight different subset of the hardware.
The bf5xx-i2s driver supports 2 channel I2S mode while the bf5xx-tdm driver
supports TDM mode and channel remapping. This patch adds support for TDM mode
and channel remapping to the bf5xx-i2s driver so that we'll eventually be able
to retire the bf5xx-tdm driver. Unfortunately the hardware is fixed to using 8
channels in TDM mode. The bf5xx-tdm driver jumps through a few hoops to make it
work well with other channel counts as well:
	* Don't support mmap
	* Translate between internal frame size (which is always 8 * sample_size)
	  and ALSA frame size (which depends on the channel count)
	* Have special copy and silence callbacks which are aware of the mismatch
	  between internal and ALSA frame size
	* Reduce the maximum buffer size to ensure that there is enough headroom for
	  dummy data.

The bf5xx-i2s driver is going to use the same mechanisms when being used int
TDM mode.

Signed-off-by: Lars-Peter Clausen <lars@metafoo.de>
Signed-off-by: Mark Brown <broonie@opensource.wolfsonmicro.com>
diff --git a/sound/soc/blackfin/bf5xx-i2s-pcm.c b/sound/soc/blackfin/bf5xx-i2s-pcm.c
index 9931a18..9cb4a80 100644
--- a/sound/soc/blackfin/bf5xx-i2s-pcm.c
+++ b/sound/soc/blackfin/bf5xx-i2s-pcm.c
@@ -40,6 +40,7 @@
 #include <asm/dma.h>
 
 #include "bf5xx-sport.h"
+#include "bf5xx-i2s-pcm.h"
 
 static void bf5xx_dma_irq(void *data)
 {
@@ -49,7 +50,6 @@
 
 static const struct snd_pcm_hardware bf5xx_pcm_hardware = {
 	.info			= SNDRV_PCM_INFO_INTERLEAVED |
-				   SNDRV_PCM_INFO_MMAP |
 				   SNDRV_PCM_INFO_MMAP_VALID |
 				   SNDRV_PCM_INFO_BLOCK_TRANSFER,
 	.formats		= SNDRV_PCM_FMTBIT_S16_LE |
@@ -66,7 +66,16 @@
 static int bf5xx_pcm_hw_params(struct snd_pcm_substream *substream,
 	struct snd_pcm_hw_params *params)
 {
-	return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
+	unsigned int buffer_size = params_buffer_bytes(params);
+	struct bf5xx_i2s_pcm_data *dma_data;
+
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
+	if (dma_data->tdm_mode)
+		buffer_size = buffer_size / params_channels(params) * 8;
+
+	return snd_pcm_lib_malloc_pages(substream, buffer_size);
 }
 
 static int bf5xx_pcm_hw_free(struct snd_pcm_substream *substream)
@@ -78,9 +87,16 @@
 
 static int bf5xx_pcm_prepare(struct snd_pcm_substream *substream)
 {
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
 	struct snd_pcm_runtime *runtime = substream->runtime;
 	struct sport_device *sport = runtime->private_data;
 	int period_bytes = frames_to_bytes(runtime, runtime->period_size);
+	struct bf5xx_i2s_pcm_data *dma_data;
+
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
+	if (dma_data->tdm_mode)
+		period_bytes = period_bytes / runtime->channels * 8;
 
 	pr_debug("%s enter\n", __func__);
 	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
@@ -127,10 +143,15 @@
 
 static snd_pcm_uframes_t bf5xx_pcm_pointer(struct snd_pcm_substream *substream)
 {
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
 	struct snd_pcm_runtime *runtime = substream->runtime;
 	struct sport_device *sport = runtime->private_data;
 	unsigned int diff;
 	snd_pcm_uframes_t frames;
+	struct bf5xx_i2s_pcm_data *dma_data;
+
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
 	pr_debug("%s enter\n", __func__);
 	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
 		diff = sport_curr_offset_tx(sport);
@@ -147,6 +168,8 @@
 		diff = 0;
 
 	frames = bytes_to_frames(substream->runtime, diff);
+	if (dma_data->tdm_mode)
+		frames = frames * runtime->channels / 8;
 
 	return frames;
 }
@@ -158,11 +181,18 @@
 	struct sport_device *sport_handle = snd_soc_dai_get_drvdata(cpu_dai);
 	struct snd_pcm_runtime *runtime = substream->runtime;
 	struct snd_dma_buffer *buf = &substream->dma_buffer;
+	struct bf5xx_i2s_pcm_data *dma_data;
 	int ret;
 
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
 	pr_debug("%s enter\n", __func__);
 
 	snd_soc_set_runtime_hwparams(substream, &bf5xx_pcm_hardware);
+	if (dma_data->tdm_mode)
+		runtime->hw.buffer_bytes_max /= 4;
+	else
+		runtime->hw.info |= SNDRV_PCM_INFO_MMAP;
 
 	ret = snd_pcm_hw_constraint_integer(runtime,
 			SNDRV_PCM_HW_PARAM_PERIODS);
@@ -198,6 +228,88 @@
 	return 0 ;
 }
 
+static int bf5xx_pcm_copy(struct snd_pcm_substream *substream, int channel,
+	snd_pcm_uframes_t pos, void *buf, snd_pcm_uframes_t count)
+{
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	unsigned int sample_size = runtime->sample_bits / 8;
+	struct bf5xx_i2s_pcm_data *dma_data;
+	unsigned int i;
+	void *src, *dst;
+
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
+	if (dma_data->tdm_mode) {
+		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+			src = buf;
+			dst = runtime->dma_area;
+			dst += pos * sample_size * 8;
+
+			while (count--) {
+				for (i = 0; i < runtime->channels; i++) {
+					memcpy(dst + dma_data->map[i] *
+						sample_size, src, sample_size);
+					src += sample_size;
+				}
+				dst += 8 * sample_size;
+			}
+		} else {
+			src = runtime->dma_area;
+			src += pos * sample_size * 8;
+			dst = buf;
+
+			while (count--) {
+				for (i = 0; i < runtime->channels; i++) {
+					memcpy(dst, src + dma_data->map[i] *
+						sample_size, sample_size);
+					dst += sample_size;
+				}
+				src += 8 * sample_size;
+			}
+		}
+	} else {
+		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+			src = buf;
+			dst = runtime->dma_area;
+			dst += frames_to_bytes(runtime, pos);
+		} else {
+			src = runtime->dma_area;
+			src += frames_to_bytes(runtime, pos);
+			dst = buf;
+		}
+
+		memcpy(dst, src, frames_to_bytes(runtime, count));
+	}
+
+	return 0;
+}
+
+static int bf5xx_pcm_silence(struct snd_pcm_substream *substream,
+	int channel, snd_pcm_uframes_t pos, snd_pcm_uframes_t count)
+{
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	unsigned int sample_size = runtime->sample_bits / 8;
+	void *buf = runtime->dma_area;
+	struct bf5xx_i2s_pcm_data *dma_data;
+	unsigned int offset, size;
+
+	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
+
+	if (dma_data->tdm_mode) {
+		offset = pos * 8 * sample_size;
+		size = count * 8 * sample_size;
+	} else {
+		offset = frames_to_bytes(runtime, pos);
+		size = frames_to_bytes(runtime, count);
+	}
+
+	snd_pcm_format_set_silence(runtime->format, buf + offset, size);
+
+	return 0;
+}
+
 static struct snd_pcm_ops bf5xx_pcm_i2s_ops = {
 	.open		= bf5xx_pcm_open,
 	.ioctl		= snd_pcm_lib_ioctl,
@@ -207,6 +319,8 @@
 	.trigger	= bf5xx_pcm_trigger,
 	.pointer	= bf5xx_pcm_pointer,
 	.mmap		= bf5xx_pcm_mmap,
+	.copy		= bf5xx_pcm_copy,
+	.silence	= bf5xx_pcm_silence,
 };
 
 static u64 bf5xx_pcm_dmamask = DMA_BIT_MASK(32);