/*
 *  linux/drivers/sound/vidc.c
 *
 *  Copyright (C) 1997-2000 by Russell King <rmk@arm.linux.org.uk>
 *
 * 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.
 *
 *  VIDC20 audio driver.
 *
 * The VIDC20 sound hardware consists of the VIDC20 itself, a DAC and a DMA
 * engine.  The DMA transfers fixed-format (16-bit little-endian linear)
 * samples to the VIDC20, which then transfers this data serially to the
 * DACs.  The samplerate is controlled by the VIDC.
 *
 * We currently support a mixer device, but it is currently non-functional.
 */

#include <linux/config.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

#include <asm/hardware.h>
#include <asm/dma.h>
#include <asm/io.h>
#include <asm/hardware/iomd.h>
#include <asm/irq.h>
#include <asm/system.h>

#include "sound_config.h"
#include "vidc.h"

#ifndef _SIOC_TYPE
#define _SIOC_TYPE(x)	_IOC_TYPE(x)
#endif
#ifndef _SIOC_NR
#define _SIOC_NR(x)	_IOC_NR(x)
#endif

#define VIDC_SOUND_CLOCK	(250000)
#define VIDC_SOUND_CLOCK_EXT	(176400)

/*
 * When using SERIAL SOUND mode (external DAC), the number of physical
 * channels is fixed at 2.
 */
static int		vidc_busy;
static int		vidc_adev;
static int		vidc_audio_rate;
static char		vidc_audio_format;
static char		vidc_audio_channels;

static unsigned char	vidc_level_l[SOUND_MIXER_NRDEVICES] = {
	85,		/* master	*/
	50,		/* bass		*/
	50,		/* treble	*/
	0,		/* synth	*/
	75,		/* pcm		*/
	0,		/* speaker	*/
	100,		/* ext line	*/
	0,		/* mic		*/
	100,		/* CD		*/
	0,
};

static unsigned char	vidc_level_r[SOUND_MIXER_NRDEVICES] = {
	85,		/* master	*/
	50,		/* bass		*/
	50,		/* treble	*/
	0,		/* synth	*/
	75,		/* pcm		*/
	0,		/* speaker	*/
	100,		/* ext line	*/
	0,		/* mic		*/
	100,		/* CD		*/
	0,
};

static unsigned int	vidc_audio_volume_l;	/* left PCM vol, 0 - 65536 */
static unsigned int	vidc_audio_volume_r;	/* right PCM vol, 0 - 65536 */

extern void	vidc_update_filler(int bits, int channels);
extern int	softoss_dev;

static void
vidc_mixer_set(int mdev, unsigned int level)
{
	unsigned int lev_l = level & 0x007f;
	unsigned int lev_r = (level & 0x7f00) >> 8;
	unsigned int mlev_l, mlev_r;

	if (lev_l > 100)
		lev_l = 100;
	if (lev_r > 100)
		lev_r = 100;

#define SCALE(lev,master)	((lev) * (master) * 65536 / 10000)

	mlev_l = vidc_level_l[SOUND_MIXER_VOLUME];
	mlev_r = vidc_level_r[SOUND_MIXER_VOLUME];

	switch (mdev) {
	case SOUND_MIXER_VOLUME:
	case SOUND_MIXER_PCM:
		vidc_level_l[mdev] = lev_l;
		vidc_level_r[mdev] = lev_r;

		vidc_audio_volume_l = SCALE(lev_l, mlev_l);
		vidc_audio_volume_r = SCALE(lev_r, mlev_r);
/*printk("VIDC: PCM vol %05X %05X\n", vidc_audio_volume_l, vidc_audio_volume_r);*/
		break;
	}
#undef SCALE
}

static int vidc_mixer_ioctl(int dev, unsigned int cmd, caddr_t arg)
{
	unsigned int val;
	unsigned int mdev;

	if (_SIOC_TYPE(cmd) != 'M')
		return -EINVAL;

	mdev = _SIOC_NR(cmd);

	if (_SIOC_DIR(cmd) & _SIOC_WRITE) {
		if (get_user(val, (unsigned int *)arg))
			return -EFAULT;

		if (mdev < SOUND_MIXER_NRDEVICES)
			vidc_mixer_set(mdev, val);
		else
			return -EINVAL;
	}

	/*
	 * Return parameters
	 */
	switch (mdev) {
	case SOUND_MIXER_RECSRC:
		val = 0;
		break;

	case SOUND_MIXER_DEVMASK:
		val = SOUND_MASK_VOLUME | SOUND_MASK_PCM | SOUND_MASK_SYNTH;
		break;

	case SOUND_MIXER_STEREODEVS:
		val = SOUND_MASK_VOLUME | SOUND_MASK_PCM | SOUND_MASK_SYNTH;
		break;

	case SOUND_MIXER_RECMASK:
		val = 0;
		break;

	case SOUND_MIXER_CAPS:
		val = 0;
		break;

	default:
		if (mdev < SOUND_MIXER_NRDEVICES)
			val = vidc_level_l[mdev] | vidc_level_r[mdev] << 8;
		else
			return -EINVAL;
	}

	return put_user(val, (unsigned int *)arg) ? -EFAULT : 0;
}

static unsigned int vidc_audio_set_format(int dev, unsigned int fmt)
{
	switch (fmt) {
	default:
		fmt = AFMT_S16_LE;
	case AFMT_U8:
	case AFMT_S8:
	case AFMT_S16_LE:
		vidc_audio_format = fmt;
		vidc_update_filler(vidc_audio_format, vidc_audio_channels);
	case AFMT_QUERY:
		break;
	}
	return vidc_audio_format;
}

#define my_abs(i) ((i)<0 ? -(i) : (i))

static int vidc_audio_set_speed(int dev, int rate)
{
	if (rate) {
		unsigned int hwctrl, hwrate, hwrate_ext, rate_int, rate_ext;
		unsigned int diff_int, diff_ext;
		unsigned int newsize, new2size;

		hwctrl = 0x00000003;

		/* Using internal clock */
		hwrate = (((VIDC_SOUND_CLOCK * 2) / rate) + 1) >> 1;
		if (hwrate < 3)
			hwrate = 3;
		if (hwrate > 255)
			hwrate = 255;

		/* Using exernal clock */
		hwrate_ext = (((VIDC_SOUND_CLOCK_EXT * 2) / rate) + 1) >> 1;
		if (hwrate_ext < 3)
			hwrate_ext = 3;
		if (hwrate_ext > 255)
			hwrate_ext = 255;

		rate_int = VIDC_SOUND_CLOCK / hwrate;
		rate_ext = VIDC_SOUND_CLOCK_EXT / hwrate_ext;

		/* Chose between external and internal clock */
		diff_int = my_abs(rate_ext-rate);
		diff_ext = my_abs(rate_int-rate);
		if (diff_ext < diff_int) {
			/*printk("VIDC: external %d %d %d\n", rate, rate_ext, hwrate_ext);*/
			hwrate=hwrate_ext;
			hwctrl=0x00000002;
			/* Allow roughly 0.4% tolerance */
			if (diff_ext > (rate/256))
				rate=rate_ext;
		} else {
			/*printk("VIDC: internal %d %d %d\n", rate, rate_int, hwrate);*/
			hwctrl=0x00000003;
			/* Allow rougly 0.4% tolerance */
			if (diff_int > (rate/256))
				rate=rate_int;
		}

		vidc_writel(0xb0000000 | (hwrate - 2));
		vidc_writel(0xb1000000 | hwctrl);

		newsize = (10000 / hwrate) & ~3;
		if (newsize < 208)
			newsize = 208;
		if (newsize > 4096)
			newsize = 4096;
		for (new2size = 128; new2size < newsize; new2size <<= 1);
		if (new2size - newsize > newsize - (new2size >> 1))
			new2size >>= 1;
		if (new2size > 4096) {
			printk(KERN_ERR "VIDC: error: dma buffer (%d) %d > 4K\n",
				newsize, new2size);
			new2size = 4096;
		}
		/*printk("VIDC: dma size %d\n", new2size);*/
		dma_bufsize = new2size;
		vidc_audio_rate = rate;
	}
	return vidc_audio_rate;
}

static short vidc_audio_set_channels(int dev, short channels)
{
	switch (channels) {
	default:
		channels = 2;
	case 1:
	case 2:
		vidc_audio_channels = channels;
		vidc_update_filler(vidc_audio_format, vidc_audio_channels);
	case 0:
		break;
	}
	return vidc_audio_channels;
}

/*
 * Open the device
 */
static int vidc_audio_open(int dev, int mode)
{
	/* This audio device does not have recording capability */
	if (mode == OPEN_READ)
		return -EPERM;

	if (vidc_busy)
		return -EBUSY;

	vidc_busy = 1;
	return 0;
}

/*
 * Close the device
 */
static void vidc_audio_close(int dev)
{
	vidc_busy = 0;
}

/*
 * Output a block via DMA to sound device.
 *
 * We just set the DMA start and count; the DMA interrupt routine
 * will take care of formatting the samples (via the appropriate
 * vidc_filler routine), and flag via vidc_audio_dma_interrupt when
 * more data is required.
 */
static void
vidc_audio_output_block(int dev, unsigned long buf, int total_count, int one)
{
	struct dma_buffparms *dmap = audio_devs[dev]->dmap_out;
	unsigned long flags;

	local_irq_save(flags);
	dma_start = buf - (unsigned long)dmap->raw_buf_phys + (unsigned long)dmap->raw_buf;
	dma_count = total_count;
	local_irq_restore(flags);
}

static void
vidc_audio_start_input(int dev, unsigned long buf, int count, int intrflag)
{
}

static int vidc_audio_prepare_for_input(int dev, int bsize, int bcount)
{
	return -EINVAL;
}

static irqreturn_t vidc_audio_dma_interrupt(void)
{
	DMAbuf_outputintr(vidc_adev, 1);
	return IRQ_HANDLED;
}

/*
 * Prepare for outputting samples.
 *
 * Each buffer that will be passed will be `bsize' bytes long,
 * with a total of `bcount' buffers.
 */
static int vidc_audio_prepare_for_output(int dev, int bsize, int bcount)
{
	struct audio_operations *adev = audio_devs[dev];

	dma_interrupt = NULL;
	adev->dmap_out->flags |= DMA_NODMA;

	return 0;
}

/*
 * Stop our current operation.
 */
static void vidc_audio_reset(int dev)
{
	dma_interrupt = NULL;
}

static int vidc_audio_local_qlen(int dev)
{
	return /*dma_count !=*/ 0;
}

static void vidc_audio_trigger(int dev, int enable_bits)
{
	struct audio_operations *adev = audio_devs[dev];

	if (enable_bits & PCM_ENABLE_OUTPUT) {
		if (!(adev->flags & DMA_ACTIVE)) {
			unsigned long flags;

			local_irq_save(flags);

			/* prevent recusion */
			adev->flags |= DMA_ACTIVE;

			dma_interrupt = vidc_audio_dma_interrupt;
			vidc_sound_dma_irq(0, NULL, NULL);
			iomd_writeb(DMA_CR_E | 0x10, IOMD_SD0CR);

			local_irq_restore(flags);
		}
	}
}

static struct audio_driver vidc_audio_driver =
{
	.owner			= THIS_MODULE,
	.open			= vidc_audio_open,
	.close			= vidc_audio_close,
	.output_block		= vidc_audio_output_block,
	.start_input		= vidc_audio_start_input,
	.prepare_for_input	= vidc_audio_prepare_for_input,
	.prepare_for_output	= vidc_audio_prepare_for_output,
	.halt_io		= vidc_audio_reset,
	.local_qlen		= vidc_audio_local_qlen,
	.trigger		= vidc_audio_trigger,
	.set_speed		= vidc_audio_set_speed,
	.set_bits		= vidc_audio_set_format,
	.set_channels		= vidc_audio_set_channels
};

static struct mixer_operations vidc_mixer_operations = {
	.owner		= THIS_MODULE,
	.id		= "VIDC",
	.name		= "VIDCsound",
	.ioctl		= vidc_mixer_ioctl
};

void vidc_update_filler(int format, int channels)
{
#define TYPE(fmt,ch) (((fmt)<<2) | ((ch)&3))

	switch (TYPE(format, channels)) {
	default:
	case TYPE(AFMT_U8, 1):
		vidc_filler = vidc_fill_1x8_u;
		break;

	case TYPE(AFMT_U8, 2):
		vidc_filler = vidc_fill_2x8_u;
		break;

	case TYPE(AFMT_S8, 1):
		vidc_filler = vidc_fill_1x8_s;
		break;

	case TYPE(AFMT_S8, 2):
		vidc_filler = vidc_fill_2x8_s;
		break;

	case TYPE(AFMT_S16_LE, 1):
		vidc_filler = vidc_fill_1x16_s;
		break;

	case TYPE(AFMT_S16_LE, 2):
		vidc_filler = vidc_fill_2x16_s;
		break;
	}
}

static void __init attach_vidc(struct address_info *hw_config)
{
	char name[32];
	int i, adev;

	sprintf(name, "VIDC %d-bit sound", hw_config->card_subtype);
	conf_printf(name, hw_config);
	memset(dma_buf, 0, sizeof(dma_buf));

	adev = sound_install_audiodrv(AUDIO_DRIVER_VERSION, name,
			&vidc_audio_driver, sizeof(vidc_audio_driver),
			DMA_AUTOMODE, AFMT_U8 | AFMT_S8 | AFMT_S16_LE,
			NULL, hw_config->dma, hw_config->dma2);

	if (adev < 0)
		goto audio_failed;

	/*
	 * 1024 bytes => 64 buffers
	 */
	audio_devs[adev]->min_fragment = 10;
	audio_devs[adev]->mixer_dev = num_mixers;

	audio_devs[adev]->mixer_dev =
		sound_install_mixer(MIXER_DRIVER_VERSION,
				name, &vidc_mixer_operations,
				sizeof(vidc_mixer_operations), NULL);

	if (audio_devs[adev]->mixer_dev < 0)
		goto mixer_failed;

	for (i = 0; i < 2; i++) {
		dma_buf[i] = get_zeroed_page(GFP_KERNEL);
		if (!dma_buf[i]) {
			printk(KERN_ERR "%s: can't allocate required buffers\n",
				name);
			goto mem_failed;
		}
		dma_pbuf[i] = virt_to_phys((void *)dma_buf[i]);
	}

	if (sound_alloc_dma(hw_config->dma, hw_config->name)) {
		printk(KERN_ERR "%s: DMA %d is in  use\n", name, hw_config->dma);
		goto dma_failed;
	}

	if (request_irq(hw_config->irq, vidc_sound_dma_irq, 0,
			hw_config->name, &dma_start)) {
		printk(KERN_ERR "%s: IRQ %d is in use\n", name, hw_config->irq);
		goto irq_failed;
	}
	vidc_adev = adev;
	vidc_mixer_set(SOUND_MIXER_VOLUME, (85 | 85 << 8));

#if defined(CONFIG_SOUND_SOFTOSS) || defined(CONFIG_SOUND_SOFTOSS_MODULE)
	softoss_dev = adev;
#endif
	return;

irq_failed:
	sound_free_dma(hw_config->dma);
dma_failed:
mem_failed:
	for (i = 0; i < 2; i++)
		free_page(dma_buf[i]);
	sound_unload_mixerdev(audio_devs[adev]->mixer_dev);
mixer_failed:
	sound_unload_audiodev(adev);
audio_failed:
	return;
}

static int __init probe_vidc(struct address_info *hw_config)
{
	hw_config->irq		= IRQ_DMAS0;
	hw_config->dma		= DMA_VIRTUAL_SOUND;
	hw_config->dma2		= -1;
	hw_config->card_subtype	= 16;
	hw_config->name		= "VIDC20";
	return 1;
}

static void __exit unload_vidc(struct address_info *hw_config)
{
	int i, adev = vidc_adev;

	vidc_adev = -1;

	free_irq(hw_config->irq, &dma_start);
	sound_free_dma(hw_config->dma);

	if (adev >= 0) {
		sound_unload_mixerdev(audio_devs[adev]->mixer_dev);
		sound_unload_audiodev(adev);
		for (i = 0; i < 2; i++)
			free_page(dma_buf[i]);
	}
}

static struct address_info cfg;

static int __init init_vidc(void)
{
	if (probe_vidc(&cfg) == 0)
		return -ENODEV;

	attach_vidc(&cfg);

	return 0;
}

static void __exit cleanup_vidc(void)
{
	unload_vidc(&cfg);
}

module_init(init_vidc);
module_exit(cleanup_vidc);

MODULE_AUTHOR("Russell King");
MODULE_DESCRIPTION("VIDC20 audio driver");
MODULE_LICENSE("GPL");
