/* linux/kernel/chr_drv/sound/dmabuf.c

The DMA buffer manager for digitized voice applications

(C) 1992  Hannu Savolainen (hsavolai@cs.helsinki.fi) */

#include "sound_config.h"

#ifdef CONFIGURE_SOUNDCARD

#include <linux/types.h>
#include <linux/errno.h>
#include <linux/signal.h>
#include <linux/fcntl.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/tty.h>
#include <linux/ctype.h>
#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <sys/kd.h>
#include <asm/dma.h>
#include <linux/wait.h>
#include <linux/soundcard.h>
#include "sound_calls.h"

#ifndef EXCLUDE_AUDIO

#include "dev_table.h"

#define OUTB outb
#define DEB(WHAT)		/* (WHAT) */
#define DEB1(WHAT)		(WHAT)

/* The DSP channel can be used either for input or output. Variable
   'dma_mode' will be set when the program calls read or write first time
   after open. Current version doesn't support mode changes without closing
   and reopening the device. Support for this feature may be implemented in a
   future version of this driver. */

#define DMODE_NONE		0
#define DMODE_OUTPUT		1
#define DMODE_INPUT		2
#define DMODE_INIT		3

#define DMA_ADDR		0x002	/* Port for low 16 bits of DMA
					   address */
#define DMA_TOP			0x083	/* Port for top 4 bits */

#define DMA_COUNT		0x003	/* Port for DMA count */
#define DMA_M2			0x00C	/* DMA status */
#define DMA_M1			0x00B	/* DMA status */
#define DMA_INIT		0x00A	/* DMA init port */
#define DMA_RESET_VAL		0x005

#define DMA_READ		0x045
#define DMA_WRITE		0x049

/* DMA_PAGESIZE can be 64k or 128k (not supported yet) */
#ifndef DMA_PAGESIZE
#define DMA_PAGESIZE	(65536)
#endif

static struct wait_queue *dma_sleeper = NULL;

static volatile int dmaqlen = 0, dmaqhead = 0, dmaqtail = 0, dma_nbufs = 0, dma_active = 0,
  dma_underrun;

static int dma_buffsize = DSP_BUFFSIZE;

static int dma_mode = DMODE_NONE;	/* DMODE_INPUT, DMODE_OUTPUT or
					   DMODE_NONE */
static char *dma_buf[DSP_BUFFCOUNT] = {NULL};	/* Pointers to valid buffers */
static int dma_counts[DSP_BUFFCOUNT];	/* Number of actual bytes in each of
					   the buffers */

static int dmabuf_busy = 0;

int
DMAbuf_open (int dev, int mode)
{
  int i, retval;

  if (dmabuf_busy)
    return -EBUSY;

  if (dev >= num_dspdevs)
    {
      printk ("No such DSP device %d\n", dev);
      return -ENODEV;
    }

  if (!dsp_devs[dev])
    {
      printk ("DSP device %d not initialized\n", dev);
      return -ENODEV;
    }

  if (dma_nbufs <= 0) return -ENOSPC;	/* Memory allocation failed during boot */

  if ((retval = dsp_devs[dev]->open (dev, mode)) < 0)
    return retval;

  dma_underrun = 0;

  dmabuf_busy = 1;

  dmaqlen = dmaqtail = dmaqhead = 0;
  for (i = 0; i < dma_nbufs; i++)
    dma_counts[i] = 0;

  return 0;
}

static void
dma_reset (int dev)
{
  dsp_devs[dev]->reset (dev);

  dmaqlen = 0;
  dmaqhead = 0;
  dmaqtail = 0;
  dma_active = 0;
}

static int
dma_sync (int dev)
{
  if (dma_mode == DMODE_OUTPUT)
    while (!(current->signal & ~current->blocked)
	   && dmaqlen)
      {
	interruptible_sleep_on (&dma_sleeper);	/* Wait until a complete
						   block is ready */
      }
  return dmaqlen;
}

int
DMAbuf_release (int dev, int mode)
{

  if (!(current->signal & ~current->blocked) && (dma_mode == DMODE_OUTPUT))
    {
      dma_sync (dev);
    }

  dma_reset (dev);

  if (!dma_active)
    dsp_devs[dev]->close (dev);

  dma_mode = DMODE_NONE;
  dmabuf_busy = 0;

  return 0;
}

int
DMAbuf_getrdbuffer (int dev, char **buf, int *len)
{

  if (!dma_mode)
    {
      int err;
      if ((err = dsp_devs[dev]->prepare_for_input ())< 0)
	return err;
      dma_mode = DMODE_INPUT;
    }

  if (dma_mode != DMODE_INPUT)
    return -EBUSY;		/* Can't change mode on fly */

  if (!dmaqlen)
    {
      if (!dma_active)
	{
	  dsp_devs[dev]->start_input (dev, dma_buf[dmaqtail], dma_buffsize);
	}

      interruptible_sleep_on (&dma_sleeper);	/* Wait until a complete
						   block is ready */
      dma_sleeper = NULL;
    }

  if (!dmaqlen)
    return -EINTR;

  *buf = &dma_buf[dmaqhead][dma_counts[dmaqhead]];
  *len = dma_buffsize - dma_counts[dmaqhead];

  return dmaqhead;
}

int
DMAbuf_rmchars (int dev, int buff_no, int c)
{
  int p = dma_counts[dmaqhead] + c;

  if (buff_no != dmaqhead)
    printk ("Soundcard warning: DMA Buffers out of sync\n");

  if (p >= dma_buffsize)
    {				/* This buffer is now empty */
      dma_counts[dmaqhead] = 0;
      dmaqlen--;
      dmaqhead = (dmaqhead + 1) % dma_nbufs;
    }
  else
    dma_counts[dmaqhead] = p;

  return 0;
}

int
DMAbuf_read (int dev, char *user_buf, int count)
{
  char *dmabuf;
  int buff_no, c, err;

  /* This routine returns at most 'count' bytes from the dsp input buffers.
     Returns negative value if there is an error. */

  if ((buff_no = DMAbuf_getrdbuffer (dev, &dmabuf, &c)) < 0)
    return buff_no;

  if (c > count)
    c = count;

  memcpy_tofs (user_buf, dmabuf, c);

  if ((err = DMAbuf_rmchars (dev, buff_no, c)) < 0)
    return err;
  return c;

}

int
DMAbuf_ioctl (int dev, unsigned int cmd, unsigned int arg)
{
  switch (cmd)
    {
      case SNDCTL_DSP_SYNC:
      return dma_sync (dev);
      break;

    case SNDCTL_DSP_GETBLKSIZE:
      return dma_buffsize;
      break;

    default:
      return dsp_devs[dev]->ioctl (dev, cmd, arg);
    }

  return -EIO;
}

int
DMAbuf_getwrbuffer (int dev, char **buf, int *size)
{

  if (!dma_mode)
    {
      int err;
      dma_mode = DMODE_OUTPUT;
      if ((err = dsp_devs[dev]->prepare_for_output ())< 0)
	return err;
    }

  if (dma_mode != DMODE_OUTPUT)
    return -EBUSY;		/* Can't change mode on fly */


  if (dmaqlen == dma_nbufs)
    {
      if (!dma_active)
	{
	  printk ("Soundcard warning: DMA not activated %d/%d\n",
		  dmaqlen, dma_nbufs);
	  return -EIO;
	}

      interruptible_sleep_on (&dma_sleeper);	/* Wait for space */
      dma_sleeper = NULL;
    }

  if (dmaqlen == dma_nbufs)
    return -EIO;		/* We have got signal (?) */

  *buf = dma_buf[dmaqtail];
  *size = dma_buffsize;
  dma_counts[dmaqtail] = 0;

  return dmaqtail;
}

int
DMAbuf_start_output (int dev, int buff_no, int l)
{
  if (buff_no != dmaqtail)
    printk ("Soundcard warning: DMA buffers out of sync %d != %d\n", buff_no, dmaqtail);

  dmaqlen++;

  dma_counts[dmaqtail] = l;

  dmaqtail = (dmaqtail + 1) % dma_nbufs;

  if (!dma_active)
    dsp_devs[dev]->output_block (dev, dma_buf[dmaqhead], dma_counts[dmaqhead]);

  return 0;
}

#ifndef LOADABLE_SOUNDCARD
static int
valid_dma_page (unsigned long addr)
{
  if (((addr & (DMA_PAGESIZE-1)) + dma_buffsize) <= DMA_PAGESIZE)
    return 1;
  else
    return 0;
}

#endif

int
DMAbuf_start_dma (int chan, long physaddr, int count, int dma_mode)
{
  /*
   * The count must be one less than the actual size.
   * This is handled by set_dma_addr()
   */

  cli ();
  disable_dma (chan);
  clear_dma_ff (chan);
  set_dma_mode (chan, (dma_mode == DMA_READ) ? DMA_MODE_READ : DMA_MODE_WRITE);
  set_dma_addr (chan, physaddr);
  set_dma_count (chan, count);
  enable_dma (chan);
  sti ();

  dma_active = 1;

  return count;
}

long
DMAbuf_init (long mem_start)
{

#ifdef LOADABLE_SOUNDCARD

  /* There should be a preloaded buffer area in the kernel. The following is
     used to find it. */
  int i;

  extern int sound_dma_nbufs;
  extern int sound_dma_buffsize;

  extern char *sound_dma_buf[];

  dma_buffsize = sound_dma_buffsize;
  dma_nbufs = sound_dma_nbufs;

  for (i = 0; i < dma_nbufs; i++)
    dma_buf[i] = sound_dma_buf[i];

#else

/* In this version the DMA buffer allocation is done by 
 * sound_mem_init() which is called by init/main.c
 */
  dma_nbufs = -1;	/* Sign for sound_mem_init() */
#endif

  dmaqlen = 0;
  dmaqhead = 0;
  dmaqtail = 0;
  dma_active = 0;

  return mem_start;
}

void
DMAbuf_outputintr (int dev)
{
  dma_active = 0;
  dmaqlen--;
  dmaqhead = (dmaqhead + 1) % dma_nbufs;

  if (dmaqlen)
    dsp_devs[dev]->output_block (dev, dma_buf[dmaqhead], dma_counts[dmaqhead]);
  else
    {
      if (dmabuf_busy)
	dma_underrun++;
      else
	{			/* Device has been closed */
	  dsp_devs[dev]->close (dev);
	}
    }

  if (dma_sleeper)
    {
      wake_up (&dma_sleeper);
    }
}

void
DMAbuf_inputintr (int dev)
{

  dma_active = 0;
  if (!dmabuf_busy)
    {
      dsp_devs[dev]->close (dev);
    }
  else if (dmaqlen == (dma_nbufs - 1))
    {
      dma_underrun++;
    }
  else
    {
      dmaqlen++;
      dmaqtail = (dmaqtail + 1) % dma_nbufs;

      dsp_devs[dev]->start_input (dev, dma_buf[dmaqtail], dma_buffsize);
    }

  if (dma_sleeper)
    {
      wake_up (&dma_sleeper);
    }
}

int
DMAbuf_open_dma (int chan)
{
  if (request_dma (chan))
    {
      printk ("Unable to grab DMA%d for the audio driver\n", chan);
      return 0;
    }
  return 1;
}

void
DMAbuf_close_dma (int chan)
{
  DMAbuf_reset_dma (chan);
  free_dma (chan);
}

void
DMAbuf_reset_dma (int chan)
{
}

/*
 *	The sound_mem_init() is called by mem_init() immediately after mem_map is
 *	initialized and before free_page_list is created.
 *
 *	This routine allocates DMA buffers at the end of available physical memory
 *	( <16M) and marks pages reserved at mem_map.
 */

void
sound_mem_init(void)
{
  int i;
  unsigned long start_addr, end_addr, mem_ptr;

  if (!dma_nbufs) return;	/* DMA buffers not necessary */

  mem_ptr = high_memory;

/* Some sanity checks */

  if (dma_buffsize > DMA_PAGESIZE) dma_buffsize = DMA_PAGESIZE;
  dma_buffsize &= 0xfffff000;
  if (dma_buffsize < 4096) dma_buffsize = 4096;
  
  if (mem_ptr > (16*1024*1024)) mem_ptr = 16*1024*1024;

/* Now allocate the buffers */

   for (dma_nbufs = 0; dma_nbufs < DSP_BUFFCOUNT; dma_nbufs++)
    {
      start_addr = mem_ptr - dma_buffsize;
      if (!valid_dma_page(start_addr))
         start_addr &= ~(DMA_PAGESIZE-1);	/* Align address to DMA_PAGESIZE */

      end_addr   = start_addr + dma_buffsize - 1;

      dma_buf[dma_nbufs] = (char *) start_addr;
      mem_ptr = start_addr;

      for (i=MAP_NR(start_addr);i<=MAP_NR(end_addr);i++) 
      {
        if (mem_map[i]) panic("sound_mem_init: Page not free (driver incompatible with kernel).\n");

      	mem_map[i] = MAP_PAGE_RESERVED;
      }
    }

}
#else
	/* Stub versions if audio services not included	*/

int DMAbuf_open(int dev, int mode) {return -ENODEV;}
int DMAbuf_release(int dev, int mode) {return 0;}
int DMAbuf_read (int dev, char *user_buf, int count) {return -EIO;}
int DMAbuf_getwrbuffer(int dev, char **buf, int *size) {return -EIO;}
int DMAbuf_getrdbuffer(int dev, char **buf, int *len) {return -EIO;}
int DMAbuf_rmchars(int dev, int buff_no, int c) {return -EIO;}
int DMAbuf_start_output(int dev, int buff_no, int l) {return -EIO;}
int DMAbuf_ioctl(int dev, unsigned int cmd, unsigned int arg) {return -EIO;}
long DMAbuf_init(long mem_start) {return mem_start;}
int DMAbuf_start_dma (int chan, long physaddr, int count, int dma_mode) {return -EIO;}
int DMAbuf_open_dma (int chan) {return -ENODEV;}
void DMAbuf_close_dma (int chan) {return;}
void DMAbuf_reset_dma (int chan) {return;}
void DMAbuf_inputintr(int dev) {return;}
void DMAbuf_outputintr(int dev) {return;}
#endif

#endif

#if !defined(CONFIGURE_SOUNDCARD) || defined(EXCLUDE_AUDIO)
void
sound_mem_init(void)
{
	/* Dummy version */
}

#endif
