/*
   forward-alsa-fd2110.c - ALSA driver for SoftLab-NSK FD2110 video board

   Copyright (C) 2017 - 2024 SoftLab-NSK <forward@softlab.tv>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "forward-alsa.h"
#include "forward-alsa-io.h"
#include "forward-fd2110.h"

#include <sound/control.h>

#include "forward-alsa-ioctl.h"
#include "fd2110_reg.h"

#include "../forward-v4l2/forward-v4l2-io.h"

#define FD2110_BUFFER_NEXT(buf, offset, count) ((((unsigned int)(buf)) + (offset)) % (count))

#define FD2110_IO_IS_SDI(io) ((io)->index < 2)
#define FD2110_IO_VDMA_STREAM(io) (((io)->index - 2) % 16)
#define FD2110_IO_IS_TX(io) ((((io)->index - 2) % 32) >= 16)
#define FD2110_IO_ETHERNET(io) (((io)->index - 2) / 32)

#define FD2110_IO_IRQ_NUM(io)                                       \
	(FD2110_IO_IS_SDI(io) ? ((io)->index * 3) :                 \
				(32 + FD2110_IO_ETHERNET(io) * 32 + \
				 (FD2110_IO_IS_TX(io) ? 16 : 0) + FD2110_IO_VDMA_STREAM(io)))
#define FD2110_IO_VDMA_BASE(io)                                                        \
	(FD2110_IO_IS_SDI(io) ? (0xE0000000 + (io)->index * 0x00800000) :              \
				(0x80000000 + FD2110_IO_ETHERNET(io) * 0x10000000 +    \
				 (FD2110_IO_IS_TX(io) ? (0x08000000) : (0x00000000)) + \
				 FD2110_IO_VDMA_STREAM(io) * 0x00800000))

#define FD2110_IO_VDMA_SIZE(io) (0x00800000)

#define FD2110_AUDIO_BUFFER_SIZE (65536)
#define FD2110_AUDIO_SAMPLES_PER_PACKET (48)
#define FD2110_AUDIO_EFFECTIVE_BUFFER_SIZE                              \
	((FD2110_AUDIO_BUFFER_SIZE / FD2110_AUDIO_SAMPLES_PER_PACKET) * \
	 FD2110_AUDIO_SAMPLES_PER_PACKET)

enum fd2110_alsa_ctl_type {
	FD2110_CTL_STREAM_ADDRESS = 0,
	FD2110_CTL_STREAM_FORMAT,
	FD2110_CTL_STREAM_NUM_CHANNELS,
	FD2110_CTL_STREAM_SRC_PORT,
	FD2110_CTL_STREAM_SSRC,
	FD2110_CTL_COUNT,
	FD2110_CTL_IN_COUNT = FD2110_CTL_STREAM_NUM_CHANNELS + 1,
	FD2110_CTL_OUT_COUNT = FD2110_CTL_COUNT,
};

struct fd2110_alsa_ctl {
	const char *name;
};

struct fd2110_alsa_io {
	struct forward_alsa_io *io;
	struct snd_forward_stream_address address;
	bool address_valid;
	enum snd_forward_stream_format stream_fmt;
	int stream_ch;
	u32 stream_ssrc;
	u16 stream_own_port;

	struct snd_kcontrol *ctl[FD2110_CTL_COUNT];
};

static const struct fd2110_alsa_ctl fd2110_alsa_ctls_in[FD2110_CTL_COUNT] = {
	[FD2110_CTL_STREAM_ADDRESS] = { .name = "PCM Capture Stream Address" },
	[FD2110_CTL_STREAM_FORMAT] = { .name = "PCM Capture Stream Format" },
	[FD2110_CTL_STREAM_NUM_CHANNELS] = { .name = "PCM Capture Stream Channels" },
};

static const struct fd2110_alsa_ctl fd2110_alsa_ctls_out[FD2110_CTL_COUNT] = {
	[FD2110_CTL_STREAM_ADDRESS] = { .name = "PCM Playback Stream Address" },
	[FD2110_CTL_STREAM_FORMAT] = { .name = "PCM Playback Stream Format" },
	[FD2110_CTL_STREAM_NUM_CHANNELS] = { .name = "PCM Playback Stream Channels" },
	[FD2110_CTL_STREAM_SRC_PORT] = { .name = "PCM Playback Stream Source Port" },
	[FD2110_CTL_STREAM_SSRC] = { .name = "PCM Playback Stream SSRC" },
};

static const char *fd2110_alsa_ctl_fmt_names[SND_FORWARD_STREAM_FORMAT_COUNT] = { "L24", "L20",
										  "L16" };

static const snd_pcm_format_t fd2110_alsa_ctl_fmt_types[SND_FORWARD_STREAM_FORMAT_COUNT] = {
	SNDRV_PCM_FORMAT_S24, SNDRV_PCM_FORMAT_S20, SNDRV_PCM_FORMAT_S16
};

static inline bool fd2110_ip_is_v6(const u8 *ipaddr)
{
	int i = 0;
	for (i = 0; i < 10; i++) {
		if (ipaddr[i] != 0x00)
			return true;
	}
	if ((ipaddr[10] != 0xFF) || (ipaddr[11] != 0xFF))
		return true;

	return false;
}

static int fd2110_update_stream_address(struct forward_alsa_io *io)
{
	struct fd2110_alsa_io *fd2110 = io->private;

	if (FD2110_IO_IS_TX(io)) {
		if (fd2110->address_valid) {
			forward_fd2110_out_mixer_update(io->alsa->dev, FD2110_IO_ETHERNET(io),
							FD2110_IO_VDMA_STREAM(io) + 16,
							fd2110->stream_own_port,
							fd2110->address.ipv6, fd2110->address.port,
							fd2110_ip_is_v6(fd2110->address.ipv6), io);
			forward_fd2110_out_mixer_toggle_en(io->alsa->dev, FD2110_IO_ETHERNET(io),
							   FD2110_IO_VDMA_STREAM(io) + 16, true);
		} else
			forward_fd2110_out_mixer_disable_all(io->alsa->dev, FD2110_IO_ETHERNET(io),
							     io);
	} else {
		if (fd2110->address_valid) {
			return forward_fd2110_in_filter_replace(
				io->alsa->dev, FD2110_IO_ETHERNET(io),
				FD2110_IO_VDMA_STREAM(io) + 16, FORWARD_ALSA_RFC3190_TAG,
				fd2110->address.ipv6, fd2110->address.port, io);
		} else {
			forward_fd2110_in_filter_remove_all(io->alsa->dev, FD2110_IO_ETHERNET(io),
							    io);
		}
	}

	return 0;
}

static int fd2110_ctl_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
{
	struct fd2110_alsa_io *fd2110 = snd_kcontrol_chip(kcontrol);

	if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_ADDRESS]) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_BYTES;
		uinfo->count = sizeof(struct snd_forward_stream_address);
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_FORMAT]) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
		uinfo->count = 1;
		uinfo->value.enumerated.items = SND_FORWARD_STREAM_FORMAT_COUNT;
		if (uinfo->value.enumerated.item >= uinfo->value.enumerated.items)
			uinfo->value.enumerated.item = uinfo->value.enumerated.items - 1;
		strcpy(uinfo->value.enumerated.name,
		       fd2110_alsa_ctl_fmt_names[uinfo->value.enumerated.item]);
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_NUM_CHANNELS]) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
		uinfo->count = 1;
		uinfo->value.integer.min = 1;
		uinfo->value.integer.max = 16;
		uinfo->value.integer.step = 1;
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SRC_PORT]) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
		uinfo->count = 1;
		uinfo->value.integer.min = 0;
		uinfo->value.integer.max = 65535;
		uinfo->value.integer.step = 1;
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SSRC]) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
		uinfo->count = 1;
		uinfo->value.integer.min = INT_MIN;
		uinfo->value.integer.max = INT_MAX;
		uinfo->value.integer.step = 1;
	} else
		return -EINVAL;
	return 0;
}

static int fd2110_ctl_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
	struct fd2110_alsa_io *fd2110 = snd_kcontrol_chip(kcontrol);
	if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_ADDRESS])
		*(struct snd_forward_stream_address *)ucontrol->value.bytes.data = fd2110->address;
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_FORMAT])
		ucontrol->value.enumerated.item[0] = (unsigned int)fd2110->stream_fmt;
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_NUM_CHANNELS])
		ucontrol->value.integer.value[0] = fd2110->stream_ch;
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SRC_PORT])
		ucontrol->value.integer.value[0] = (long)fd2110->stream_own_port;
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SSRC])
		ucontrol->value.integer.value[0] = (long)fd2110->stream_ssrc;
	else
		return -EINVAL;
	return 0;
}

static int fd2110_ctl_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
	struct fd2110_alsa_io *fd2110 = snd_kcontrol_chip(kcontrol);
	int i;

	if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_ADDRESS]) {
		fd2110->address = *(struct snd_forward_stream_address *)ucontrol->value.bytes.data;

		fd2110->address_valid = false;
		for (i = 0; i < 16; i++) {
			if (fd2110->address.ipv6[i] != 0) {
				fd2110->address_valid = true;
				break;
			}
		}

		if (fd2110->address_valid && (fd2110->address.port != 0))
			fd2110->address_valid = true;
		else
			fd2110->address_valid = false;

		fd2110_update_stream_address(fd2110->io);
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_FORMAT])
		fd2110->stream_fmt =
			(enum snd_forward_stream_format)ucontrol->value.enumerated.item[0];
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_NUM_CHANNELS])
		fd2110->stream_ch = ucontrol->value.integer.value[0];
	else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SRC_PORT]) {
		fd2110->stream_own_port = (u16)ucontrol->value.integer.value[0];
		fd2110_update_stream_address(fd2110->io);
	} else if (kcontrol == fd2110->ctl[FD2110_CTL_STREAM_SSRC])
		fd2110->stream_ssrc = (u32)ucontrol->value.integer.value[0];
	else
		return -EINVAL;

	return 0;
}

static int fd2110_init_io(struct forward_alsa_io *io)
{
	struct fd2110_alsa_io *fd2110;
	int i;

	if (FD2110_IO_IS_SDI(io))
		return 0;

	fd2110 = devm_kzalloc(io->alsa->dev->dev, sizeof(struct fd2110_alsa_io), GFP_KERNEL);
	if (!fd2110)
		return -ENOMEM;
	fd2110->io = io;
	io->private = fd2110;

	fd2110->stream_ch = 4;
	fd2110->stream_fmt = SND_FORWARD_STREAM_FORMAT_L24;
	fd2110->stream_own_port = 10000;

	if (FD2110_IO_IS_TX(io)) {
		for (i = 0; i < FD2110_CTL_OUT_COUNT; i++) {
			struct snd_kcontrol_new ctl = { 0 };
			ctl.iface = SNDRV_CTL_ELEM_IFACE_PCM;
			ctl.name = fd2110_alsa_ctls_out[i].name;
			ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
			ctl.info = fd2110_ctl_info;
			ctl.get = fd2110_ctl_get;
			ctl.put = fd2110_ctl_put;
			ctl.device = io->pcm->device;
			ctl.subdevice = SNDRV_PCM_STREAM_PLAYBACK;
			fd2110->ctl[i] = snd_ctl_new1(&ctl, fd2110);
			snd_ctl_add(io->alsa->card, fd2110->ctl[i]);
		}
	} else {
		for (i = 0; i < FD2110_CTL_IN_COUNT; i++) {
			struct snd_kcontrol_new ctl = { 0 };
			ctl.iface = SNDRV_CTL_ELEM_IFACE_PCM;
			ctl.name = fd2110_alsa_ctls_in[i].name;
			ctl.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
			ctl.info = fd2110_ctl_info;
			ctl.get = fd2110_ctl_get;
			ctl.put = fd2110_ctl_put;
			ctl.device = io->pcm->device;
			ctl.subdevice = SNDRV_PCM_STREAM_CAPTURE;
			fd2110->ctl[i] = snd_ctl_new1(&ctl, fd2110);
			snd_ctl_add(io->alsa->card, fd2110->ctl[i]);
		}
	}

	return 0;
}

static void fd2110_fini_io(struct forward_alsa_io *io)
{
	struct fd2110_alsa_io *fd2110 = io->private;
	int i;

	if (FD2110_IO_IS_SDI(io))
		return;

	if (!fd2110)
		return;

	forward_fd2110_in_filter_remove_all(io->alsa->dev, FD2110_IO_ETHERNET(io), io);

	for (i = 0; i < FD2110_CTL_COUNT; i++) {
		if (fd2110->ctl[i])
			snd_ctl_remove(io->alsa->card, fd2110->ctl[i]);
		fd2110->ctl[i] = NULL;
	}

	devm_kfree(io->alsa->dev->dev, fd2110);
}

static forward_irq_t fd2110_irq_mask(const struct forward_alsa_io *io)
{
	forward_irq_t result = { 0 };
	forward_irq_flags_set_one(result, FD2110_IO_IRQ_NUM(io));
	return result;
}

inline u32 fd2110_samples_to_bytes(const struct forward_alsa_io *io, u32 samples)
{
	struct fd2110_alsa_io *fd2110 = io->private;

	switch (fd2110->stream_fmt) {
	case SND_FORWARD_STREAM_FORMAT_L24:
		samples *= 3;
		break;
	case SND_FORWARD_STREAM_FORMAT_L20:
		samples = samples * 20 / 8;
		break;
	case SND_FORWARD_STREAM_FORMAT_L16:
		samples *= 2;
		break;
	default:
		samples *= 1;
		break;
	}
	samples *= fd2110->stream_ch;

	return samples;
}

inline bool fd2110_irq_info(const struct forward_alsa_io *io, const forward_irq_t *irq, int *offset,
			    size_t *size, bool *field)
{
	if (!forward_irq_has_irq(*irq, FD2110_IO_IRQ_NUM(io)))
		return false;

	if (FD2110_IO_IS_TX(io)) {
		FD2110_VDMATxAudioPosition audio;
		FD2110_VDMATxVideoPosition video;

		audio = FD2110_VDMATxAudioPosition_R(io->alsa->dev->csr, FD2110_IO_ETHERNET(io),
						     FD2110_IO_VDMA_STREAM(io));
		video = FD2110_VDMATxVideoPosition_R(io->alsa->dev->csr, FD2110_IO_ETHERNET(io),
						     FD2110_IO_VDMA_STREAM(io));

		*offset = fd2110_samples_to_bytes(io, audio.position);
		*size = fd2110_samples_to_bytes(io, FD2110_AUDIO_EFFECTIVE_BUFFER_SIZE);
		*field = io->interlaced ? !video.field : false;
	} else {
		FD2110_VDMARxDataPosition audio;
		FD2110_VDMARxVideoPosition video;
		s32 delta;

		audio = FD2110_VDMARxDataPosition_R(io->alsa->dev->csr, FD2110_IO_ETHERNET(io),
						    FD2110_IO_VDMA_STREAM(io));
		video = FD2110_VDMARxVideoPosition_R(io->alsa->dev->csr, FD2110_IO_ETHERNET(io),
						     FD2110_IO_VDMA_STREAM(io));

		if (io->hw_wr_position < 0) {
			delta = 0;
			*offset = audio.position;
		} else {
			delta = (s32)audio.position - io->hw_wr_position;
			*offset = io->hw_wr_position;
		}

		if (delta < 0)
			delta += FD2110_IO_VDMA_SIZE(io);
		if ((delta < 0) || (delta > FD2110_IO_VDMA_SIZE(io)))
			delta = 0;

		*size = delta;
		*field = io->interlaced ? !video.field : false;
	}

	return true;
}

static void fd2110_buffer_info(const struct forward_alsa_io *io, u32 *address, size_t *size)
{
	*address = FD2110_IO_VDMA_BASE(io);
	if (FD2110_IO_IS_TX(io))
		*size = fd2110_samples_to_bytes(io, FD2110_AUDIO_EFFECTIVE_BUFFER_SIZE);
	else
		*size = FD2110_IO_VDMA_SIZE(io);
}

static void fd2110_fill_hw(struct forward_alsa_io *io, struct snd_pcm_hardware *hw)
{
}

static void fd2110_update_format(struct forward_alsa_io *io)
{
	struct forward_dev *dev = io->alsa->dev;
	struct fd2110_alsa_io *fd2110 = io->private;

	if (FD2110_IO_IS_SDI(io)) {
		FD2110_VideoInCS ics = FD2110_VideoInCS_R(dev->csr, io->index);
		if (ics.mode == 4) {
			io->hw_format = SNDRV_PCM_FORMAT_S20;
			io->type = FORWARD_ALSA_SMPTE_272;
		} else {
			io->hw_format = SNDRV_PCM_FORMAT_S24;
			io->type = FORWARD_ALSA_SMPTE_299;
		}
		io->subtype = 0;
		io->hw_channels = 1;
	} else {
		if (FD2110_IO_IS_TX(io)) {
			switch (fd2110->stream_fmt) {
			case SND_FORWARD_STREAM_FORMAT_L24:
				io->type = FORWARD_ALSA_RAW_24BE;
				break;
			case SND_FORWARD_STREAM_FORMAT_L20:
				io->type = FORWARD_ALSA_RAW_20BE;
				break;
			case SND_FORWARD_STREAM_FORMAT_L16:
				io->type = FORWARD_ALSA_RAW_16BE;
				break;
			default:
				io->type = FORWARD_ALSA_RAW_24BE;
				break;
			}
			io->subtype = fd2110->address.payload_type;
			io->hw_format = fd2110_alsa_ctl_fmt_types[fd2110->stream_fmt];
			io->hw_channels = fd2110->stream_ch;
		} else {
			io->type = FORWARD_ALSA_RFC3190;
			io->subtype = fd2110->address.payload_type;
			io->hw_format = fd2110_alsa_ctl_fmt_types[fd2110->stream_fmt];
			io->hw_channels = fd2110->stream_ch;
		}
	}
}

static int fd2110_start(struct forward_alsa_io *io)
{
	struct forward_dev *dev = io->alsa->dev;
	struct fd2110_alsa_io *fd2110 = io->private;
	struct forward_v4l2_io *v4l2 = NULL;

	if (io->alsa->dev->io[io->index].v4l2)
		v4l2 = (struct forward_v4l2_io *)io->alsa->dev->io[io->index].v4l2;

	if (!FD2110_IO_IS_SDI(io)) {
		if (FD2110_IO_IS_TX(io)) {
			u32 *csr = dev->csr;
			FD2110_VDMATxAudioControl ctl;
			FD2110_VDMATxAudioFormat fmt;
			FD2110_VDMATxAudioPacketization pkt;
			FD2110_VDMATxAudioPacketSize siz;
			FD2110_VDMATxAudioTimings tim;
			FD2110_VDMATxAudioTimingsRTP rtp;
			FD2110_VDMATxAudioSync sync;
			FD2110_VDMATxAudioRTPTimestamp ts;
			FD2110_VDMATxAudioRTPSSRC ssrc;
			u64 time_cur;
			u64 start_time;
			int frame_size;

			ctl.address = FD2110_IO_VDMA_BASE(io) / 0x100000;
			ctl.enable = 1;
			ctl.size = 0;
			ctl.destination = FD2110_IO_VDMA_STREAM(io) + 16;

			fmt.sampleSize = fd2110_samples_to_bytes(io, 1);
			fmt.packetSizeDenom = 0xFFFF;

			pkt.pt = fd2110->address.payload_type;

			siz.packetSizeInt = 48;
			siz.packetSizeNum = 0;

			tim.period = 148500000 / 1000 * 4;

			rtp.periodIsSample = 1;
			rtp.period = 1;

			time_cur = (u64)FD2110_VideoClockCounterL_R(csr).counter;
			time_cur |= ((u64)FD2110_VideoClockCounterH_R(csr).counter << 32);

			if (v4l2) {
				struct forward_v4l2_timings *t = &v4l2->timings;
				int frame_size = t->sdi.width * t->sdi.height;
				switch (t->sdi.mode) {
				case FORWARD_SDI_MODE_SD_SDI:
					frame_size *= 11;
					break;
				case FORWARD_SDI_MODE_HD_SDI:
					frame_size *= 2;
					break;
				case FORWARD_SDI_MODE_3G_SDI:
					frame_size *= 1;
					break;
				case FORWARD_SDI_MODE_6G_SDI:
					frame_size /= 2;
					break;
				case FORWARD_SDI_MODE_12G_SDI:
					frame_size /= 4;
					break;
				default:
					frame_size *= 1;
					break;
				}
			} else {
				frame_size = 2970000;
			}

			// FIXME: /1.001 modes
			start_time = (time_cur / frame_size + 1) * frame_size;
			if ((start_time - time_cur) < 148500) // ~1ms
				start_time += frame_size;
			sync.time = start_time & 0xFFFFFFFF;
			ts.time = (start_time * 4 / 12375) & 0xFFFFFFFF;

			ssrc.ssrc = fd2110->stream_ssrc;

			FD2110_VDMATxAudioFormat_W(csr, FD2110_IO_ETHERNET(io),
						   FD2110_IO_VDMA_STREAM(io), fmt);
			FD2110_VDMATxAudioPacketization_W(csr, FD2110_IO_ETHERNET(io),
							  FD2110_IO_VDMA_STREAM(io), pkt);
			FD2110_VDMATxAudioPacketSize_W(csr, FD2110_IO_ETHERNET(io),
						       FD2110_IO_VDMA_STREAM(io), siz);
			FD2110_VDMATxAudioTimings_W(csr, FD2110_IO_ETHERNET(io),
						    FD2110_IO_VDMA_STREAM(io), tim);
			FD2110_VDMATxAudioTimingsRTP_W(csr, FD2110_IO_ETHERNET(io),
						       FD2110_IO_VDMA_STREAM(io), rtp);
			FD2110_VDMATxAudioSync_W(csr, FD2110_IO_ETHERNET(io),
						 FD2110_IO_VDMA_STREAM(io), sync);
			FD2110_VDMATxAudioRTPTimestamp_W(csr, FD2110_IO_ETHERNET(io),
							 FD2110_IO_VDMA_STREAM(io), ts);
			FD2110_VDMATxAudioRTPSSRC_W(csr, FD2110_IO_ETHERNET(io),
						    FD2110_IO_VDMA_STREAM(io), ssrc);
			FD2110_VDMATxAudioControl_W(csr, FD2110_IO_ETHERNET(io),
						    FD2110_IO_VDMA_STREAM(io), ctl);
		} else {
			FD2110_VDMARxDataControl ctl = FD2110_VDMARxDataControl_R(
				dev->csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io));
			ctl.address = FD2110_IO_VDMA_BASE(io) / 0x100000;
			ctl.enable = 1;
			ctl.size = 0;
			FD2110_VDMARxDataControl_W(dev->csr, FD2110_IO_ETHERNET(io),
						   FD2110_IO_VDMA_STREAM(io), ctl);
		}
	}
	dev->cfg.toggle_streaming(dev, io->index, true);
	return 0;
}

static void fd2110_stop(struct forward_alsa_io *io)
{
	struct forward_dev *dev = io->alsa->dev;
	dev->cfg.toggle_streaming(dev, io->index, false);
	if (!FD2110_IO_IS_SDI(io)) {
		if (FD2110_IO_IS_TX(io)) {
			FD2110_VDMATxAudioControl ctl = FD2110_VDMATxAudioControl_R(
				dev->csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io));
			ctl.enable = 0;
			FD2110_VDMATxAudioControl_W(dev->csr, FD2110_IO_ETHERNET(io),
						    FD2110_IO_VDMA_STREAM(io), ctl);
		} else {
			FD2110_VDMARxDataControl ctl = FD2110_VDMARxDataControl_R(
				dev->csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io));
			ctl.enable = 0;
			FD2110_VDMARxDataControl_W(dev->csr, FD2110_IO_ETHERNET(io),
						   FD2110_IO_VDMA_STREAM(io), ctl);
		}
	}
}

struct forward_alsa_dev_ops fd2110_alsa_ops = {
	.init_io = fd2110_init_io,
	.fini_io = fd2110_fini_io,
	.irq_mask = fd2110_irq_mask,
	.irq_info = fd2110_irq_info,
	.buffer_info = fd2110_buffer_info,
	.fill_hw = fd2110_fill_hw,
	.update_format = fd2110_update_format,
	.start = fd2110_start,
	.stop = fd2110_stop,
};
