/*
 * Sony CDU-31A CDROM interface device driver.
 *
 * Corey Minyard (minyard@wf-rch.cirr.com)
 *
 * Colossians 3:17
 *
 * The Sony interface device driver handles Sony interface CDROM
 * drives and provides a complete block-level interface as well as an
 * ioctl() interface compatible with the Sun (as specified in
 * include/linux/cdrom.h).  With this interface, CDROMs can be
 * accessed and standard audio CDs can be played back normally.
 *
 * This interface is (unfortunatly) a polled interface.  This is
 * because most Sony interfaces are set up with DMA and interrupts
 * disables.  Some (like mine) do not even have the capability to
 * handle interrupts or DMA.  For this reason you will see a lot of
 * the following:
 *
 *   retry_count = jiffies+ SONY_JIFFIES_TIMEOUT;
 *   while ((retry_count > jiffies) && (! <some condition to wait for))
 *   {
 *      while (handle_sony_cd_attention())
 *         ;
 *
 *      sony_sleep();
 *   }
 *   if (the condition not met)
 *   {
 *      return an error;
 *   }
 *
 * This ugly hack waits for something to happen, sleeping a little
 * between every try.  it also handles attentions, which are
 * asyncronous events from the drive informing the driver that a disk
 * has been inserted, removed, etc.
 *
 * One thing about these drives: They talk in MSF (Minute Second Frame) format.
 * There are 75 frames a second, 60 seconds a minute, and up to 75 minutes on a
 * disk.  The funny thing is that these are sent to the drive in BCD, but the
 * interface wants to see them in decimal.  A lot of conversion goes on.
 *
 *  Copyright (C) 1993  Corey Minyard
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */



#include <linux/errno.h>
#include <linux/signal.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/hdreg.h>
#include <linux/genhd.h>
#include <linux/ioport.h>

#include <asm/system.h>
#include <asm/io.h>
#include <asm/segment.h>

#include <linux/cdrom.h>
#include <linux/cdu31a.h>

#define MAJOR_NR CDU31A_CDROM_MAJOR
#include "blk.h"


static unsigned short cdu31a_addresses[] =
{
   0x340,	/* Standard configuration Sony Interface */
   0x1f88,	/* Fusion CD-16 */
   0x360,	/* Secondary standard Sony Interface */
   0x320,	/* Secondary standard Sony Interface */
   0x330,	/* Secondary standard Sony Interface */
   0
};


static int handle_sony_cd_attention(void);
static int read_subcode(void);
static void sony_get_toc(void);
static int scd_open(struct inode *inode, struct file *filp);


/* The base I/O address of the Sony Interface.  This is a variable (not a
   #define) so it can be easily changed via some future ioctl() */
static unsigned short sony_cd_base_io = 0;

/*
 * The following are I/O addresses of the various registers for the drive.  The
 * comment for the base address also applies here.
 */
static volatile unsigned short sony_cd_cmd_reg;
static volatile unsigned short sony_cd_param_reg;
static volatile unsigned short sony_cd_write_reg;
static volatile unsigned short sony_cd_control_reg;
static volatile unsigned short sony_cd_status_reg;
static volatile unsigned short sony_cd_result_reg;
static volatile unsigned short sony_cd_read_reg;
static volatile unsigned short sony_cd_fifost_reg;


static int initialized = 0;                /* Has the drive been initialized? */
static int sony_disc_changed = 1;          /* Has the disk been changed
                                              since the last check? */
static int sony_toc_read = 0;              /* Has the table of contents been
                                              read? */
static int sony_spun_up = 0;               /* Has the drive been spun up? */
static unsigned int sony_buffer_size;      /* Size in bytes of the read-ahead
                                              buffer. */
static unsigned int sony_buffer_sectors;   /* Size (in 2048 byte records) of
                                              the read-ahead buffer. */
static unsigned int sony_usage = 0;        /* How many processes have the
                                              drive open. */

static volatile int sony_first_block = -1; /* First OS block (512 byte) in
                                              the read-ahead buffer */
static volatile int sony_last_block = -1;  /* Last OS block (512 byte) in
                                              the read-ahead buffer */

static struct s_sony_toc *sony_toc;              /* Points to the table of
                                                    contents. */
static struct s_sony_subcode * volatile last_sony_subcode; /* Points to the last
                                                    subcode address read */
static unsigned char * volatile sony_buffer;     /* Points to the read-ahead
                                                    buffer */

static volatile int sony_inuse = 0;  /* Is the drive in use?  Only one operation at a time
                                        allowed */

static struct wait_queue * sony_wait = NULL;

static struct task_struct *has_cd_task = NULL;  /* The task that is currently using the
                                                   CDROM drive, or NULL if none. */

/*
 * The audio status uses the values from read subchannel data as specified
 * in include/linux/cdrom.h.
 */
static volatile int sony_audio_status = CDROM_AUDIO_NO_STATUS;

/*
 * The following are a hack for pausing and resuming audio play.  The drive
 * does not work as I would expect it, if you stop it then start it again,
 * the drive seeks back to the beginning and starts over.  This holds the
 * position during a pause so a resume can restart it.  It uses the
 * audio status variable above to tell if it is paused.
 */
unsigned volatile char cur_pos_msf[3] = { 0, 0, 0 };
unsigned volatile char final_pos_msf[3] = { 0, 0, 0 };

/*
 * This routine returns 1 if the disk has been changed since the last
 * check or 0 if it hasn't.  Setting flag to 0 resets the changed flag.
 */
int
check_cdu31a_media_change(int full_dev, int flag)
{
   int retval, target;


   target = MINOR(full_dev);

   if (target > 0) {
      printk("Sony CD-ROM request error: invalid device.\n");
      return 0;
   }

   retval = sony_disc_changed;
   if (!flag)
   {
      sony_disc_changed = 0;
   }

   return retval;
}


/*
 * Wait a little while (used for polling the drive).  If in initialization,
 * setting a timeout doesn't work, so just loop for a while.
 */
static inline void
sony_sleep(void)
{
   current->state = TASK_INTERRUPTIBLE;
   current->timeout = jiffies;
   schedule();
}


/*
 * The following are convenience routine to read various status and set
 * various conditions in the drive.
 */
static inline int
is_attention(void)
{
   return((inb(sony_cd_status_reg) & SONY_ATTN_BIT) != 0);
}

static inline int
is_busy(void)
{
   return((inb(sony_cd_status_reg) & SONY_BUSY_BIT) != 0);
}

static inline int
is_data_ready(void)
{
   return((inb(sony_cd_status_reg) & SONY_DATA_RDY_BIT) != 0);
}

static inline int
is_data_requested(void)
{
   return((inb(sony_cd_status_reg) & SONY_DATA_REQUEST_BIT) != 0);
}

static inline int
is_result_ready(void)
{
   return((inb(sony_cd_status_reg) & SONY_RES_RDY_BIT) != 0);
}

static inline int
is_param_write_rdy(void)
{
   return((inb(sony_cd_fifost_reg) & SONY_PARAM_WRITE_RDY_BIT) != 0);
}

static inline void
reset_drive(void)
{
   outb(SONY_DRIVE_RESET_BIT, sony_cd_control_reg);
}

static inline void
clear_attention(void)
{
   outb(SONY_ATTN_CLR_BIT, sony_cd_control_reg);
}

static inline void
clear_result_ready(void)
{
   outb(SONY_RES_RDY_CLR_BIT, sony_cd_control_reg);
}

static inline void
clear_data_ready(void)
{
   outb(SONY_DATA_RDY_CLR_BIT, sony_cd_control_reg);
}

static inline void
clear_param_reg(void)
{
   outb(SONY_PARAM_CLR_BIT, sony_cd_control_reg);
}

static inline unsigned char
read_status_register(void)
{
   return(inb(sony_cd_status_reg));
}

static inline unsigned char
read_result_register(void)
{
   return(inb(sony_cd_result_reg));
}

static inline unsigned char
read_data_register(void)
{
   return(inb(sony_cd_read_reg));
}

static inline void
write_param(unsigned char param)
{
   outb(param, sony_cd_param_reg);
}

static inline void
write_cmd(unsigned char cmd)
{
   outb(cmd, sony_cd_cmd_reg);
   outb(SONY_RES_RDY_INT_EN_BIT, sony_cd_control_reg);
}


/*
 * This routine writes data to the parameter register.  Since this should
 * happen fairly fast, it is polled with no OS waits between.
 */
static int
write_params(unsigned char *params,
             int num_params)
{
   unsigned int retry_count;


   retry_count = SONY_READY_RETRIES;
   while ((retry_count > 0) && (!is_param_write_rdy()))
   {
      retry_count--;
   }
   if (!is_param_write_rdy())
   {
      return -EIO;
   }

   while (num_params > 0)
   {
      write_param(*params);
      params++;
      num_params--;
   }

   return 0;
}


/*
 * The following reads data from the command result register.  It is a
 * fairly complex routine, all status info flows back through this
 * interface.  The algorithm is stolen directly from the flowcharts in
 * the drive manual.
 */
static void
get_result(unsigned char *result_buffer,
           unsigned int *result_size)
{
   unsigned char a, b;
   int i;
   unsigned int retry_count;


   while (handle_sony_cd_attention())
      ;
   /* Wait for the result data to be ready */
   retry_count = jiffies + SONY_JIFFIES_TIMEOUT;
   while ((retry_count > jiffies) && (is_busy() || (!(is_result_ready()))))
   {
      sony_sleep();

      while (handle_sony_cd_attention())
         ;
   }
   if (is_busy() || (!(is_result_ready())))
   {
      result_buffer[0] = 0x20;
      result_buffer[1] = SONY_TIMEOUT_OP_ERR;
      *result_size = 2;
      return;
   }

   /*
    * Get the first two bytes.  This determines what else needs
    * to be done.
    */
   clear_result_ready();
   a = read_result_register();
   *result_buffer = a;
   result_buffer++;
   b = read_result_register();
   *result_buffer = b;
   result_buffer++;
   *result_size = 2;

   /*
    * 0x20 means an error occured.  Byte 2 will have the error code.
    * Otherwise, the command succeded, byte 2 will have the count of
    * how many more status bytes are coming.
    *
    * The result register can be read 10 bytes at a time, a wait for
    * result ready to be asserted must be done between every 10 bytes.
    */
   if ((a & 0xf0) != 0x20)
   {
      if (b > 8)
      {
         for (i=0; i<8; i++)
         {
            *result_buffer = read_result_register();
            result_buffer++;
            (*result_size)++;
         }
         b = b - 8;

         while (b > 10)
         {
            retry_count = SONY_READY_RETRIES;
            while ((retry_count > 0) && (!is_result_ready()))
            {
               retry_count--;
            }
            if (!is_result_ready())
            {
               result_buffer[0] = 0x20;
               result_buffer[1] = SONY_TIMEOUT_OP_ERR;
               *result_size = 2;
               return;
            }

            clear_result_ready();
                                
            for (i=0; i<10; i++)
            {
               *result_buffer = read_result_register();
               result_buffer++;
               (*result_size)++;
            }
            b = b - 10;
         }

         if (b > 0)
         {
            retry_count = SONY_READY_RETRIES;
            while ((retry_count > 0) && (!is_result_ready()))
            {
               retry_count--;
            }
            if (!is_result_ready())
            {
               result_buffer[0] = 0x20;
               result_buffer[1] = SONY_TIMEOUT_OP_ERR;
               *result_size = 2;
               return;
            }
         }
      }

      while (b > 0)
      {
         *result_buffer = read_result_register();
         result_buffer++;
         (*result_size)++;
         b--;
      }
   }
}


/*
 * This routine issues a read data command and gets the data.  I don't
 * really like the way this is done (I would prefer for do_sony_cmd() to
 * handle it automatically) but I found that the drive returns status
 * when it finishes reading (not when the host has read all the data)
 * or after it gets an error.  This means that the status can be
 * received at any time and should be handled immediately (at least
 * between every 2048 byte block) to check for errors, we can't wait
 * until all the data is read.
 */
static void
get_data(unsigned char *data,
         unsigned char *params,         /* 6 bytes with the MSF start address
                                           and number of sectors to read. */
         unsigned int data_size,
         unsigned char *result_buffer,
         unsigned int *result_size)
{
   int i;
   unsigned int cur_offset;
   unsigned int retry_count;
   int result_read;
   int num_retries;


   cli();
   if (current != has_cd_task) /* Allow recursive calls to this routine */
   {
      while (sony_inuse)
      {
         interruptible_sleep_on(&sony_wait);
         if (current->signal & ~current->blocked)
         {
            result_buffer[0] = 0x20;
            result_buffer[1] = SONY_SIGNAL_OP_ERR;
            *result_size = 2;
            return;
         }
      }
   }
   sony_inuse = 1;
   has_cd_task = current;
   sti();

   num_retries = 0;
retry_data_operation:

   /*
    * Clear any outstanding attentions and wait for the drive to
    * complete any pending operations.
    */
   while (handle_sony_cd_attention())
      ;

   retry_count = jiffies + SONY_JIFFIES_TIMEOUT;
   while ((retry_count > jiffies) && (is_busy()))
   {
      sony_sleep();
      
      while (handle_sony_cd_attention())
         ;
   }
   if (is_busy())
   {
      result_buffer[0] = 0x20;
      result_buffer[1] = SONY_TIMEOUT_OP_ERR;
      *result_size = 2;
      goto get_data_end;
   }

   /* Issue the command */
   clear_result_ready();
   clear_param_reg();

   write_params(params, 6);
   write_cmd(SONY_READ_CMD);

   /*
    * Read the data from the drive one 2048 byte sector at a time.  Handle
    * any results received between sectors, if an error result is returned
    * terminate the operation immediately.
    */
   cur_offset = 0;
   result_read = 0;
   while (data_size > 0)
   {
      /* Wait for the drive to tell us we have something */
      retry_count = jiffies + SONY_JIFFIES_TIMEOUT;
      while ((retry_count > jiffies) && (!(is_result_ready() || is_data_ready())))
      {
         while (handle_sony_cd_attention())
            ;
         
         sony_sleep();
      }
      if (!(is_result_ready() || is_data_ready()))
      {
         result_buffer[0] = 0x20;
         result_buffer[1] = SONY_TIMEOUT_OP_ERR;
         *result_size = 2;
         goto get_data_end;
      }
      
      /* Handle results first */
      if (is_result_ready())
      {
         result_read = 1;
         get_result(result_buffer, result_size);
         if ((*result_size < 2) || (result_buffer[0] != 0))
         {
            goto get_data_end;
         }
      }
      else /* Handle data next */
      {
         /*
          * The drive has to be polled for status on a byte-by-byte basis
          * to know if the data is ready.  Yuck.  I really wish I could use DMA.
          */
         clear_data_ready();
         for (i=0; i<2048; i++)
         {
            retry_count = jiffies + SONY_JIFFIES_TIMEOUT;
            while ((retry_count > jiffies) && (!is_data_requested()))
            {
               while (handle_sony_cd_attention())
                  ;
               
               sony_sleep();
            }
            if (!is_data_requested())
            {
               result_buffer[0] = 0x20;
               result_buffer[1] = SONY_TIMEOUT_OP_ERR;
               *result_size = 2;
               goto get_data_end;
            }
            
            *data = read_data_register();
            data++;
            data_size--;
         }
         cur_offset = cur_offset + 2048;
      }
   }

   /* Make sure the result has been read */
   if (!result_read)
   {
      get_result(result_buffer, result_size);
   }

get_data_end:
   if (   ((result_buffer[0] & 0x20) == 0x20)
       && (num_retries < MAX_CDU31A_RETRIES))
   {
      num_retries++;
      current->state = TASK_INTERRUPTIBLE;
      current->timeout = jiffies + 10; /* Wait .1 seconds on retries */
      schedule();
      goto retry_data_operation;
   }

   has_cd_task = NULL;
   sony_inuse = 0;
   wake_up_interruptible(&sony_wait);
}


/*
 * Do a command that does not involve data transfer.
 */
static void
do_sony_cd_cmd(unsigned char cmd,
               unsigned char *params,
               unsigned int num_params,
               unsigned char *result_buffer,
               unsigned int *result_size)
{
   unsigned int retry_count;
   int num_retries;


   cli();
   if (current != has_cd_task) /* Allow recursive calls to this routine */
   {
      while (sony_inuse)
      {
         interruptible_sleep_on(&sony_wait);
         if (current->signal & ~current->blocked)
         {
            result_buffer[0] = 0x20;
            result_buffer[1] = SONY_SIGNAL_OP_ERR;
            *result_size = 2;
            return;
         }
      }
   }
   sony_inuse = 1;
   has_cd_task = current;
   sti();

   num_retries = 0;
retry_cd_operation:

   while (handle_sony_cd_attention())
      ;
   
   retry_count = jiffies + SONY_JIFFIES_TIMEOUT;
   while ((retry_count > jiffies) && (is_busy()))
   {
      sony_sleep();
      
      while (handle_sony_cd_attention())
         ;
   }
   if (is_busy())
   {
      result_buffer[0] = 0x20;
      result_buffer[1] = SONY_TIMEOUT_OP_ERR;
      *result_size = 2;
      goto do_cmd_end;
   }

   clear_result_ready();
   clear_param_reg();

   write_params(params, num_params);
   write_cmd(cmd);

   get_result(result_buffer, result_size);

do_cmd_end:
   if (   ((result_buffer[0] & 0x20) == 0x20)
       && (num_retries < MAX_CDU31A_RETRIES))
   {
      num_retries++;
      current->state = TASK_INTERRUPTIBLE;
      current->timeout = jiffies + 10; /* Wait .1 seconds on retries */
      schedule();
      goto retry_cd_operation;
   }

   has_cd_task = NULL;
   sony_inuse = 0;
   wake_up_interruptible(&sony_wait);
}


/*
 * Handle an attention from the drive.  This will return 1 if it found one
 * or 0 if not (if one is found, the caller might want to call again).
 */
static int
handle_sony_cd_attention(void)
{
   unsigned char atten_code;
   unsigned char res_reg[2];
   unsigned int res_size;


   if (is_attention())
   {
      clear_attention();
      atten_code = read_result_register();

      switch (atten_code)
      {
       /* Someone changed the CD.  Mark it as changed */
      case SONY_MECH_LOADED_ATTN:
         sony_disc_changed = 1;
         sony_toc_read = 0;
         sony_audio_status = CDROM_AUDIO_NO_STATUS;
         sony_first_block = -1;
         sony_last_block = -1;
         if (initialized)
         {
            do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
            sony_get_toc();
         }
         break;

      case SONY_AUDIO_PLAY_DONE_ATTN:
         sony_audio_status = CDROM_AUDIO_COMPLETED;
         read_subcode();
         break;

      case SONY_EJECT_PUSHED_ATTN:
         sony_audio_status = CDROM_AUDIO_INVALID;
         break;

      case SONY_LEAD_IN_ERR_ATTN:
      case SONY_LEAD_OUT_ERR_ATTN:
      case SONY_DATA_TRACK_ERR_ATTN:
      case SONY_AUDIO_PLAYBACK_ERR_ATTN:
         sony_audio_status = CDROM_AUDIO_ERROR;
         break;
      }
      return(1);
   }

   return(0);
}


/* Convert from an integer 0-99 to BCD */
static inline unsigned int
int_to_bcd(unsigned int val)
{
   int retval;


   retval = (val / 10) << 4;
   retval = retval | val % 10;
   return(retval);
}


/* Convert from BCD to an integer from 0-99 */
static unsigned int
bcd_to_int(unsigned int bcd)
{
   return((((bcd >> 4) & 0x0f) * 10) + (bcd & 0x0f));
}


/*
 * Convert a logical sector value (like the OS would want to use for
 * a block device) to an MSF format.
 */
static void
log_to_msf(unsigned int log, unsigned char *msf)
{
   log = log + LOG_START_OFFSET;
   msf[0] = int_to_bcd(log / 4500);
   log = log % 4500;
   msf[1] = int_to_bcd(log / 75);
   msf[2] = int_to_bcd(log % 75);
}


/*
 * Convert an MSF format to a logical sector.
 */
static unsigned int
msf_to_log(unsigned char *msf)
{
   unsigned int log;


   log = bcd_to_int(msf[2]);
   log += bcd_to_int(msf[1]) * 75;
   log += bcd_to_int(msf[0]) * 4500;
   log = log - LOG_START_OFFSET;

   return log;
}


/*
 * Take in integer size value and put it into a buffer like
 * the drive would want to see a number-of-sector value.
 */
static void
size_to_buf(unsigned int size,
            unsigned char *buf)
{
   buf[0] = size / 65536;
   size = size % 65536;
   buf[1] = size / 256;
   buf[2] = size % 256;
}


/*
 * The OS calls this to perform a read or write operation to the drive.
 * Write obviously fail.  Reads to a read ahead of sony_buffer_size
 * bytes to help speed operations.  This especially helps since the OS
 * uses 1024 byte blocks and the drive uses 2048 byte blocks.  Since most
 * data access on a CD is done sequentially, this saves a lot of operations.
 */
static void
do_cdu31a_request(void)
{
   int block;
   unsigned int dev;
   int nsect;
   unsigned char params[10];
   unsigned char res_reg[2];
   unsigned int res_size;
   int copyoff;
   int spin_up_retry;
   unsigned int read_size;


   if (!sony_spun_up)
   {
      scd_open (NULL,NULL);
   }

   while (1)
   {
      /*
       * The beginning here is stolen from the hard disk driver.  I hope
       * its right.
       */
      if (!(CURRENT) || CURRENT->dev < 0)
      {
         return;
      }

      INIT_REQUEST;
      dev = MINOR(CURRENT->dev);
      block = CURRENT->sector;
      nsect = CURRENT->nr_sectors;
      if (dev != 0)
      {
         end_request(0);
         continue;
      }

      switch(CURRENT->cmd)
      {
      case READ:
         /*
          * If the block address is invalid or the request goes beyond the end of
          * the media, return an error.
          */
         if ((block / 4) >= sony_toc->lead_out_start_lba)
         {
            end_request(0);
            return;
         }
         if (((block + nsect) / 4) >= sony_toc->lead_out_start_lba)
         {
            end_request(0);
            return;
         }

         while (nsect > 0)
         {
            /*
             * If the requested sector is not currently in the read-ahead buffer,
             * it must be read in.
             */
            if ((block < sony_first_block) || (block > sony_last_block))
            {
               sony_first_block = (block / 4) * 4;
               log_to_msf(block/4, params);

               /*
                * If the full read-ahead would go beyond the end of the media, trim
                * it back to read just till the end of the media.
                */
               if (((block / 4) + sony_buffer_sectors) >= sony_toc->lead_out_start_lba)
               {
                  sony_last_block = (sony_toc->lead_out_start_lba * 4) - 1;
                  read_size = sony_toc->lead_out_start_lba - (block / 4);
               }
               else
               {
                  sony_last_block = sony_first_block + (sony_buffer_sectors * 4) - 1;
                  read_size = sony_buffer_sectors;
               }
               size_to_buf(read_size, &params[3]);

               /*
                * Read the data.  If the drive was not spinning, spin it up and try
                * once more.  I know, the goto is ugly, but I am too lazy to fix it.
                */
               spin_up_retry = 0;
try_read_again:
               get_data(sony_buffer, params, (read_size * 2048), res_reg, &res_size);
               if ((res_size < 2) || (res_reg[0] != 0))
               {
                  if ((res_reg[1] == SONY_NOT_SPIN_ERR) && (!spin_up_retry))
                  {
                     do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
                     spin_up_retry = 1;
                     goto try_read_again;
                  }

                  printk("Sony CDROM Read error: 0x%2.2x\n", res_reg[1]);
                  sony_first_block = -1;
                  sony_last_block = -1;
                  end_request(0);
                  return;
               }
            }
   
            /*
             * The data is in memory now, copy it to the buffer and advance to the
             * next block to read.
             */
            copyoff = (block - sony_first_block) * 512;
            memcpy(CURRENT->buffer, sony_buffer+copyoff, 512);
               
            block += 1;
            nsect -= 1;
            CURRENT->buffer += 512;
         }
               
         end_request(1);
         break;
            
      case WRITE:
         end_request(0);
         break;
            
      default:
         panic("Unkown SONY CD cmd");
      }
   }
}


/*
 * Read the table of contents from the drive and set sony_toc_read if
 * successful.
 */
static void
sony_get_toc(void)
{
   unsigned int res_size;


   if (!sony_toc_read)
   {
      do_sony_cd_cmd(SONY_REQ_TOC_DATA_CMD,
                     NULL,
                     0, 
                     (unsigned char *) sony_toc, 
                     &res_size);
      if ((res_size < 2) || ((sony_toc->exec_status[0] & 0x20) == 0x20))
      {
         return;
      }
      sony_toc->lead_out_start_lba = msf_to_log(sony_toc->lead_out_start_msf);
      sony_toc_read = 1;
   }
}


/*
 * Search for a specific track in the table of contents.
 */
static int
find_track(int track)
{
   int i;
   int num_tracks;


   num_tracks = sony_toc->last_track_num + sony_toc->first_track_num + 1;
   for (i = 0; i < num_tracks; i++)
   {
      if (sony_toc->tracks[i].track == track)
      {
         return i;
      }
   }

   return -1;
}


/*
 * Read the subcode and put it int last_sony_subcode for future use.
 */
static int
read_subcode(void)
{
   unsigned int res_size;


   do_sony_cd_cmd(SONY_REQ_SUBCODE_ADDRESS_CMD,
                  NULL,
                  0, 
                  (unsigned char *) last_sony_subcode, 
                  &res_size);
   if ((res_size < 2) || ((last_sony_subcode->exec_status[0] & 0x20) == 0x20))
   {
      printk("Sony CDROM error 0x%2.2x (read_subcode)\n",
             last_sony_subcode->exec_status[1]);
      return -EIO;
   }

   return 0;
}


/*
 * Get the subchannel info like the CDROMSUBCHNL command wants to see it.  If
 * the drive is playing, the subchannel needs to be read (since it would be
 * changing).  If the drive is paused or completed, the subcode information has
 * already been stored, just use that.  The ioctl call wants things in decimal
 * (not BCD), so all the conversions are done.
 */
static int
sony_get_subchnl_info(long arg)
{
   struct cdrom_subchnl schi;


   /* Get attention stuff */
   while (handle_sony_cd_attention())
      ;

   sony_get_toc();
   if (!sony_toc_read)
   {
      return -EIO;
   }

   verify_area(VERIFY_READ, (char *) arg, sizeof(schi));
   verify_area(VERIFY_WRITE, (char *) arg, sizeof(schi));

   memcpy_fromfs(&schi, (char *) arg, sizeof(schi));
   
   switch (sony_audio_status)
   {
   case CDROM_AUDIO_PLAY:
      if (read_subcode() < 0)
      {
         return -EIO;
      }
      break;

   case CDROM_AUDIO_PAUSED:
   case CDROM_AUDIO_COMPLETED:
      break;

   case CDROM_AUDIO_NO_STATUS:
      schi.cdsc_audiostatus = sony_audio_status;
      memcpy_tofs((char *) arg, &schi, sizeof(schi));
      return 0;
      break;

   case CDROM_AUDIO_INVALID:
   case CDROM_AUDIO_ERROR:
   default:
      return -EIO;
   }

   schi.cdsc_audiostatus = sony_audio_status;
   schi.cdsc_adr = last_sony_subcode->address;
   schi.cdsc_ctrl = last_sony_subcode->control;
   schi.cdsc_trk = bcd_to_int(last_sony_subcode->track_num);
   schi.cdsc_ind = bcd_to_int(last_sony_subcode->index_num);
   if (schi.cdsc_format == CDROM_MSF)
   {
      schi.cdsc_absaddr.msf.minute = bcd_to_int(last_sony_subcode->abs_msf[0]);
      schi.cdsc_absaddr.msf.second = bcd_to_int(last_sony_subcode->abs_msf[1]);
      schi.cdsc_absaddr.msf.frame = bcd_to_int(last_sony_subcode->abs_msf[2]);

      schi.cdsc_reladdr.msf.minute = bcd_to_int(last_sony_subcode->rel_msf[0]);
      schi.cdsc_reladdr.msf.second = bcd_to_int(last_sony_subcode->rel_msf[1]);
      schi.cdsc_reladdr.msf.frame = bcd_to_int(last_sony_subcode->rel_msf[2]);
   }
   else if (schi.cdsc_format == CDROM_LBA)
   {
      schi.cdsc_absaddr.lba = msf_to_log(last_sony_subcode->abs_msf);
      schi.cdsc_reladdr.lba = msf_to_log(last_sony_subcode->rel_msf);
   }
   
   memcpy_tofs((char *) arg, &schi, sizeof(schi));
   return 0;
}


/*
 * The big ugly ioctl handler.
 */
static int
scd_ioctl(struct inode *inode,
          struct file  *file,
          unsigned int  cmd,
          unsigned long arg)
{
   unsigned int dev;
   unsigned char res_reg[2];
   unsigned int res_size;
   unsigned char params[7];
   int i;


   if (!inode)
   {
      return -EINVAL;
   }
   dev = MINOR(inode->i_rdev) >> 6;
   if (dev != 0)
   {
      return -EINVAL;
   }

   switch (cmd)
   {
   case CDROMSTART:     /* Spin up the drive */
      do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
      if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMSTART)\n", res_reg[1]);
         return -EIO;
      }
      return 0;
      break;
      
   case CDROMSTOP:      /* Spin down the drive */
      do_sony_cd_cmd(SONY_AUDIO_STOP_CMD, NULL, 0, res_reg, &res_size);

      /*
       * Spin the drive down, ignoring the error if the disk was
       * already not spinning.
       */
      sony_audio_status = CDROM_AUDIO_NO_STATUS;
      do_sony_cd_cmd(SONY_SPIN_DOWN_CMD, NULL, 0, res_reg, &res_size);
      if (   ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
          && (res_reg[1] != SONY_NOT_SPIN_ERR))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMSTOP)\n", res_reg[1]);
         return -EIO;
      }
      
      return 0;
      break;

   case CDROMPAUSE:     /* Pause the drive */
      do_sony_cd_cmd(SONY_AUDIO_STOP_CMD, NULL, 0, res_reg, &res_size);
      if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMPAUSE)\n", res_reg[1]);
         return -EIO;
      }

      /* Get the current position and save it for resuming */
      if (read_subcode() < 0)
      {
         return -EIO;
      }
      cur_pos_msf[0] = last_sony_subcode->abs_msf[0];
      cur_pos_msf[1] = last_sony_subcode->abs_msf[1];
      cur_pos_msf[2] = last_sony_subcode->abs_msf[2];
      sony_audio_status = CDROM_AUDIO_PAUSED;
      return 0;
      break;

   case CDROMRESUME:    /* Start the drive after being paused */
      if (sony_audio_status != CDROM_AUDIO_PAUSED)
      {
         return -EINVAL;
      }
      
      do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
      
      /* Start the drive at the saved position. */
      params[1] = cur_pos_msf[0];
      params[2] = cur_pos_msf[1];
      params[3] = cur_pos_msf[2];
      params[4] = final_pos_msf[0];
      params[5] = final_pos_msf[1];
      params[6] = final_pos_msf[2];
      params[0] = 0x03;
      do_sony_cd_cmd(SONY_AUDIO_PLAYBACK_CMD, params, 7, res_reg, &res_size);
      if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMRESUME)\n", res_reg[1]);
         return -EIO;
      }
      sony_audio_status = CDROM_AUDIO_PLAY;
      return 0;
      break;

   case CDROMPLAYMSF:   /* Play starting at the given MSF address. */
      verify_area(VERIFY_READ, (char *) arg, 6);
      do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
      memcpy_fromfs(&(params[1]), (void *) arg, 6);
      
      /* The parameters are given in int, must be converted */
      for (i=1; i<7; i++)
      {
         params[i] = int_to_bcd(params[i]);
      }
      params[0] = 0x03;
      do_sony_cd_cmd(SONY_AUDIO_PLAYBACK_CMD, params, 7, res_reg, &res_size);
      if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMPLAYMSF)\n", res_reg[1]);
         return -EIO;
      }
      
      /* Save the final position for pauses and resumes */
      final_pos_msf[0] = params[4];
      final_pos_msf[1] = params[5];
      final_pos_msf[2] = params[6];
      sony_audio_status = CDROM_AUDIO_PLAY;
      return 0;
      break;

   case CDROMREADTOCHDR:        /* Read the table of contents header */
      {
         struct cdrom_tochdr *hdr;
         struct cdrom_tochdr loc_hdr;
         
         sony_get_toc();
         if (!sony_toc_read)
         {
            return -EIO;
         }
         
         hdr = (struct cdrom_tochdr *) arg;
         verify_area(VERIFY_WRITE, hdr, sizeof(*hdr));
         loc_hdr.cdth_trk0 = bcd_to_int(sony_toc->first_track_num);
         loc_hdr.cdth_trk1 = bcd_to_int(sony_toc->last_track_num);
         memcpy_tofs(hdr, &loc_hdr, sizeof(*hdr));
      }
      return 0;
      break;

   case CDROMREADTOCENTRY:      /* Read a given table of contents entry */
      {
         struct cdrom_tocentry *entry;
         struct cdrom_tocentry loc_entry;
         int track_idx;
         unsigned char *msf_val = NULL;
         
         sony_get_toc();
         if (!sony_toc_read)
         {
            return -EIO;
         }
         
         entry = (struct cdrom_tocentry *) arg;
         verify_area(VERIFY_READ, entry, sizeof(*entry));
         verify_area(VERIFY_WRITE, entry, sizeof(*entry));
         
         memcpy_fromfs(&loc_entry, entry, sizeof(loc_entry));
         
         /* Lead out is handled separately since it is special. */
         if (loc_entry.cdte_track == CDROM_LEADOUT)
         {
            loc_entry.cdte_adr = sony_toc->address2;
            loc_entry.cdte_ctrl = sony_toc->control2;
            msf_val = sony_toc->lead_out_start_msf;
         }
         else
         {
            track_idx = find_track(int_to_bcd(loc_entry.cdte_track));
            if (track_idx < 0)
            {
               return -EINVAL;
            }
            
            loc_entry.cdte_adr = sony_toc->tracks[track_idx].address;
            loc_entry.cdte_ctrl = sony_toc->tracks[track_idx].control;
            msf_val = sony_toc->tracks[track_idx].track_start_msf;
         }
         
         /* Logical buffer address or MSF format requested? */
         if (loc_entry.cdte_format == CDROM_LBA)
         {
            loc_entry.cdte_addr.lba = msf_to_log(msf_val);
         }
         else if (loc_entry.cdte_format == CDROM_MSF)
         {
            loc_entry.cdte_addr.msf.minute = bcd_to_int(*msf_val);
            loc_entry.cdte_addr.msf.second = bcd_to_int(*(msf_val+1));
            loc_entry.cdte_addr.msf.frame = bcd_to_int(*(msf_val+2));
         }
         memcpy_tofs(entry, &loc_entry, sizeof(*entry));
      }
      return 0;
      break;

   case CDROMPLAYTRKIND:     /* Play a track.  This currently ignores index. */
      {
         struct cdrom_ti ti;
         int track_idx;
         
         sony_get_toc();
         if (!sony_toc_read)
         {
            return -EIO;
         }
         
         verify_area(VERIFY_READ, (char *) arg, sizeof(ti));
         
         memcpy_fromfs(&ti, (char *) arg, sizeof(ti));
         if (   (ti.cdti_trk0 < sony_toc->first_track_num)
             || (ti.cdti_trk0 > sony_toc->last_track_num)
             || (ti.cdti_trk1 < ti.cdti_trk0))
         {
            return -EINVAL;
         }
         
         track_idx = find_track(int_to_bcd(ti.cdti_trk0));
         if (track_idx < 0)
         {
            return -EINVAL;
         }
         params[1] = sony_toc->tracks[track_idx].track_start_msf[0];
         params[2] = sony_toc->tracks[track_idx].track_start_msf[1];
         params[3] = sony_toc->tracks[track_idx].track_start_msf[2];
         
         /*
          * If we want to stop after the last track, use the lead-out
          * MSF to do that.
          */
         if (ti.cdti_trk1 >= bcd_to_int(sony_toc->last_track_num))
         {
            log_to_msf(msf_to_log(sony_toc->lead_out_start_msf)-1,
                       &(params[4]));
         }
         else
         {
            track_idx = find_track(int_to_bcd(ti.cdti_trk1+1));
            if (track_idx < 0)
            {
               return -EINVAL;
            }
            log_to_msf(msf_to_log(sony_toc->tracks[track_idx].track_start_msf)-1,
                       &(params[4]));
         }
         params[0] = 0x03;
         
         do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);
         
         do_sony_cd_cmd(SONY_AUDIO_PLAYBACK_CMD, params, 7, res_reg, &res_size);
         if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
         {
            printk("Params: %x %x %x %x %x %x %x\n", params[0], params[1],
                   params[2], params[3], params[4], params[5], params[6]);
            printk("Sony CDROM error 0x%2.2x (CDROMPLAYTRKIND\n", res_reg[1]);
            return -EIO;
         }
         
         /* Save the final position for pauses and resumes */
         final_pos_msf[0] = params[4];
         final_pos_msf[1] = params[5];
         final_pos_msf[2] = params[6];
         sony_audio_status = CDROM_AUDIO_PLAY;
         return 0;
      }
     
   case CDROMSUBCHNL:   /* Get subchannel info */
      return sony_get_subchnl_info(arg);

   case CDROMVOLCTRL:   /* Volume control.  What volume does this change, anyway? */
      {
         struct cdrom_volctrl volctrl;
         
         verify_area(VERIFY_READ, (char *) arg, sizeof(volctrl));
         
         memcpy_fromfs(&volctrl, (char *) arg, sizeof(volctrl));
         params[0] = SONY_SD_AUDIO_VOLUME;
         params[1] = volctrl.channel0;
         params[2] = volctrl.channel1;
         do_sony_cd_cmd(SONY_SET_DRIVE_PARAM_CMD, params, 3, res_reg, &res_size);
         if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
         {
            printk("Sony CDROM error 0x%2.2x (CDROMVOLCTRL)\n", res_reg[1]);
            return -EIO;
         }
      }
      return 0;

   case CDROMEJECT:     /* Eject the drive */
      do_sony_cd_cmd(SONY_AUDIO_STOP_CMD, NULL, 0, res_reg, &res_size);
      do_sony_cd_cmd(SONY_SPIN_DOWN_CMD, NULL, 0, res_reg, &res_size);

      sony_audio_status = CDROM_AUDIO_INVALID;
      do_sony_cd_cmd(SONY_EJECT_CMD, NULL, 0, res_reg, &res_size);
      if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
      {
         printk("Sony CDROM error 0x%2.2x (CDROMEJECT)\n", res_reg[1]);
         return -EIO;
      }
      return 0;
      break;
     
   default:
      return -EINVAL;
   }
}


/*
 * Open the drive for operations.  Spin the drive up and read the table of
 * contents if these have not already been done.
 */
static int
scd_open(struct inode *inode,
         struct file *filp)
{
   unsigned char res_reg[2];
   unsigned int res_size;
   int num_spin_ups;


   if (!sony_spun_up)
   {
      num_spin_ups = 0;

respinup_on_open:
      do_sony_cd_cmd(SONY_SPIN_UP_CMD, NULL, 0, res_reg, &res_size);

      /* The drive sometimes returns error 0.  I don't know why, but ignore
         it.  It seems to mean the drive has already done the operation. */
      if ((res_size < 2) || ((res_reg[0] != 0) && (res_reg[1] != 0)))
      {
         printk("Sony CDROM error 0x%2.2x (scd_open, spin up)\n", res_reg[1]);
         return -EIO;
      }
      
      do_sony_cd_cmd(SONY_READ_TOC_CMD, NULL, 0, res_reg, &res_size);

      /* The drive sometimes returns error 0.  I don't know why, but ignore
         it.  It seems to mean the drive has already done the operation. */
      if ((res_size < 2) || ((res_reg[0] != 0) && (res_reg[1] != 0)))
      {
         /* If the drive is already playing, its ok.  */
         if ((res_reg[1] == SONY_AUDIO_PLAYING_ERR) || (res_reg[1] == 0))
         {
            goto drive_spinning;
         }

         /* If the drive says it is not spun up (even though we just did it!)
            then retry the operation at least a few times. */
         if (   (res_reg[1] == SONY_NOT_SPIN_ERR)
             && (num_spin_ups < MAX_CDU31A_RETRIES))
         {
            num_spin_ups++;
            goto respinup_on_open;
         }

         printk("Sony CDROM error 0x%2.2x (scd_open, read toc)\n", res_reg[1]);
         do_sony_cd_cmd(SONY_SPIN_DOWN_CMD, NULL, 0, res_reg, &res_size);
         
         return -EIO;
      }

      sony_get_toc();
      if (!sony_toc_read)
      {
         do_sony_cd_cmd(SONY_SPIN_DOWN_CMD, NULL, 0, res_reg, &res_size);
         return -EIO;
      }

      sony_spun_up = 1;
   }

drive_spinning:

   if (inode)
   {
      check_disk_change(inode->i_rdev);
   }

   sony_usage++;

   return 0;
}


/*
 * Close the drive.  Spin it down if no task is using it.  The spin
 * down will fail if playing audio, so audio play is OK.
 */
static void
scd_release(struct inode *inode,
         struct file *filp)
{
   unsigned char res_reg[2];
   unsigned int  res_size;


   if (sony_usage > 0)
   {
      sony_usage--;
   }
   if (sony_usage == 0)
   {
      sync_dev(inode->i_rdev);
      do_sony_cd_cmd(SONY_SPIN_DOWN_CMD, NULL, 0, res_reg, &res_size);

      sony_spun_up = 0;
   }
}


static struct file_operations scd_fops = {
   NULL,                   /* lseek - default */
   block_read,             /* read - general block-dev read */
   block_write,            /* write - general block-dev write */
   NULL,                   /* readdir - bad */
   NULL,                   /* select */
   scd_ioctl,              /* ioctl */
   NULL,                   /* mmap */
   scd_open,               /* open */
   scd_release,            /* release */
   NULL                    /* fsync */
};


/* The different types of disc loading mechanisms supported */
static char *load_mech[] = { "caddy", "tray", "pop-up", "unknown" };

/* Read-ahead buffer sizes for different drives.  These are just arbitrary
   values, I don't know what is really optimum. */
static unsigned int mem_size[] = { 4096, 8192, 16384, 2048 };

void
get_drive_configuration(unsigned short base_io,
                        unsigned char res_reg[],
                        unsigned int *res_size)
{
   int retry_count;


   /* Set the base address */
   sony_cd_base_io = base_io;

   /* Set up all the register locations */
   sony_cd_cmd_reg = sony_cd_base_io + SONY_CMD_REG_OFFSET;
   sony_cd_param_reg = sony_cd_base_io + SONY_PARAM_REG_OFFSET;
   sony_cd_write_reg = sony_cd_base_io + SONY_WRITE_REG_OFFSET;
   sony_cd_control_reg = sony_cd_base_io + SONY_CONTROL_REG_OFFSET;
   sony_cd_status_reg = sony_cd_base_io + SONY_STATUS_REG_OFFSET;
   sony_cd_result_reg = sony_cd_base_io + SONY_RESULT_REG_OFFSET;
   sony_cd_read_reg = sony_cd_base_io + SONY_READ_REG_OFFSET;
   sony_cd_fifost_reg = sony_cd_base_io + SONY_FIFOST_REG_OFFSET;

   /*
    * Check to see if anything exists at the status register location.
    * I don't know if this is a good way to check, but it seems to work
    * ok for me.
    */
   if (read_status_register() != 0xff)
   {
      /*
       * Reset the drive and wait for attention from it (to say its reset).
       * If you don't wait, the next operation will probably fail.
       */
      reset_drive();
      retry_count = jiffies + SONY_RESET_TIMEOUT;
      while ((retry_count > jiffies) && (!is_attention()))
      {
         sony_sleep();
      }

      /* If attention is never seen probably not a CDU31a present */
      if (!is_attention())
      {
         res_reg[0] = 0x20;
         return;
      }

      /*
       * Get the drive configuration.
       */
      do_sony_cd_cmd(SONY_REQ_DRIVE_CONFIG_CMD,
                     NULL,
                     0,
                     (unsigned char *) res_reg,
                     res_size);
      return;
   }

   /* Return an error */
   res_reg[0] = 0x20;
}


/*
 * Initialize the driver.
 */
unsigned long
cdu31a_init(unsigned long mem_start, unsigned long mem_end)
{
   struct s_sony_drive_config drive_config;
   unsigned char params[3];
   unsigned char res_reg[2];
   unsigned int res_size;
   int i;
   int drive_found;


   /*
    * According to Alex Freed (freed@europa.orion.adobe.com), this is
    * required for the Fusion CD-16 package.  If the sound driver is
    * loaded, it should work fine, but just in case...
    *
    * The following turn on the CD-ROM interface for a Fusion CD-16.
    */
   outb(0xbc, 0x9a01);
   outb(0xe2, 0x9a01);

   i = 0;
   drive_found = 0;
   while (   (cdu31a_addresses[i] != 0)
          && (!drive_found))
   {
      if (check_region(cdu31a_addresses[i], 4)) {
	  i++;
	  continue;
      }
      get_drive_configuration(cdu31a_addresses[i],
                               drive_config.exec_status,
                               &res_size);
      if ((res_size > 2) && ((drive_config.exec_status[0] & 0x20) == 0x00))
      {
         drive_found = 1;
	 snarf_region(cdu31a_addresses[i], 4);

         if (register_blkdev(MAJOR_NR,"cdu31a",&scd_fops))
         {
            printk("Unable to get major %d for CDU-31a\n", MAJOR_NR);
            return mem_start;
         }

         sony_buffer_size = mem_size[SONY_HWC_GET_BUF_MEM_SIZE(drive_config)];
         sony_buffer_sectors = sony_buffer_size / 2048;

         printk("Sony I/F CDROM : %8.8s %16.16s %8.8s with %s load mechanism\n",
                drive_config.vendor_id,
                drive_config.product_id,
                drive_config.product_rev_level,
                load_mech[SONY_HWC_GET_LOAD_MECH(drive_config)]);
         printk("  using %d byte buffer", sony_buffer_size);
         if (SONY_HWC_AUDIO_PLAYBACK(drive_config))
         {
            printk(", capable of audio playback");
         }
         printk("\n");

         params[0] = SONY_SD_MECH_CONTROL;
         params[1] = 0x03;
         do_sony_cd_cmd(SONY_SET_DRIVE_PARAM_CMD,
                        params,
                        2,
                        res_reg,
                        &res_size);
         if ((res_size < 2) || ((res_reg[0] & 0x20) == 0x20))
         {
            printk("  Unable to set mechanical parameters: 0x%2.2x\n", res_reg[1]);
         }

         blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
         read_ahead[MAJOR_NR] = 8;               /* 8 sector (4kB) read-ahead */

         sony_toc = (struct s_sony_toc *) mem_start;
         mem_start += sizeof(*sony_toc);
         last_sony_subcode = (struct s_sony_subcode *) mem_start;
         mem_start += sizeof(*last_sony_subcode);
         sony_buffer = (unsigned char *) mem_start;
         mem_start += sony_buffer_size;

         initialized = 1;
      }

      i++;
   }
   
   return mem_start;
}

