[comp.os.minix] FDC for WD1797

Dickson@pco-multics.hbi.honeywell.com (Paul Dickson) (10/18/88)

Well here it is, the Floppy controller for the Western Digital 1797.
I'm sending to the whole mailing list so that it get's to the several
people who asked for it and to all the archives so it is saved for
others.  Besides, if you have two examples, it makes that much easier to
write a driver for some other kind of controller.

This message has about four lines of trailer at the end (at least that's
how much I'm adding) which you will have to delete before compiling.

This is compilable using Turbo C 1.0.

          -Paul Dickson
            Dickson%pco @ BCO-MULTICS.HBI.HONEYWELL.COM

------------------------------START OF FILE----------------------------------
/* This file contains a driver for a Floppy Disk Controller (FDC) using the
 * WD 1797 chip.  The driver supports two operations: read a block and
 * write a block.  It accepts two messages, one for reading and one for
 * writing, both using message format m2 and with the same parameters:
 *
 *    m_type      DEVICE    PROC_NR     COUNT    POSITION  ADRRESS
 * ----------------------------------------------------------------
 * |  DISK_READ | device  | proc nr |  bytes  |  offset | buf ptr |
 * |------------+---------+---------+---------+---------+---------|
 * | DISK_WRITE | device  | proc nr |  bytes  |  offset | buf ptr |
 * ----------------------------------------------------------------
 *
 * The file contains one entry point:
 *
 *   floppy_task:       main entry when system is brought up
 *
 *  ---------------------------------------------------------------------------
 *  Notes:
 *    The WD 1790 series of floppy disk controllers have one major difference
 *    from the NEC 765 series used in the typical PC-compatible, and that is
 *    they try to start reading/writing data at the BEGINNING of the index
 *    pulse, not the end. This is only a problem if you format a disk on the
 *    1790 and try to use it on the 765, as the 1790 will wait for the later
 *    sector-ID marks put on the disk by the 765.
 *
 *    Also, the 1790 series do not have internal drive selection and drive
 *    motor control, so this will be specific to the computer the controller
 *    is located in.
 *
 *    This driver is written for a Slicer single board computer that uses a
 *    DMA controller to interface the FDC to the main system. Other
 *    implementations may require the reading each byte of data from the FDC by
 *    the CPU (note that this would slow down MINIX greatly.)
 *
 *    As of October 1988 this driver has been tested fairly well, but has not
 *    undergone significant stress testing (multiple processes doing disk I/O,
 *    etc.).
 *  ---------------------------------------------------------------------------
 *  Written (from the original MINIX floppy driver)
 *  by Paul Dickson and Kevin Fleming
 *     Paul Dickson can be reached at Dickson%pco@BCO-MULTICS.HBI.HONEYWELL.COM
 *     Kevin Fleming can be reached at 72271,1677 on Compuserve
 */

#include "../h/const.h"
#include "../h/type.h"
#include "../h/callnr.h"
#include "../h/com.h"
#include "../h/error.h"
#include "const.h"
#include "type.h"
#include "glo.h"
#include "proc.h"

/* I/O Ports used by floppy disk task. */
#define FDC_STATUS      0x00    /* floppy disk controller status register */
#define FDC_COMMAND     0x00    /* floppy disk controller command register */
#define FDC_TRACK       0x02    /* floppy disk controller track register */
#define FDC_SECTOR      0x04    /* floppy disk controller sector register */
#define FDC_DATA        0x06    /* floppy disk controller data register */

/* Status bits from controller */
#define NOT_READY       0x80
#define WRITE_PROTECT   0x40
#define WRITE_FAULT     0x20
#define NOT_FOUND       0x10
#define SEEK_ERROR      0x10
#define CRC_ERROR       0x08
#define LOST_DATA       0x04

/* Floppy disk controller command bytes. */
#define FDC_SEEK        0x1C  /* command the drive to seek */
#define FDC_SEEK_NV     0x10  /* command for seeking with no verify */
#define FDC_READ        0x88  /* command the drive to read */
#define FDC_WRITE       0xA8  /* command the drive to write */
#define FDC_RECALIBRATE 0x0C  /* command the drive to go to cyl 0 */

/* DMA channel 0 port definitions. */
#define DMA_SRC_LOW   0xFFC0
#define DMA_SRC_HIGH  0xFFC2
#define DMA_DST_LOW   0xFFC4
#define DMA_DST_HIGH  0xFFC6
#define DMA_LENGTH    0xFFC8
#define DMA_COMMAND   0xFFCA

/* DMA channel commands. */
#define DMA_READ      0xA240    /* DMA read opcode */
#define DMA_WRITE     0x1680    /* DMA write opcode */
#define DMA_START     0x0006    /* start DMA transfer */

/* Parameters for the disk drive. */
#define SECTOR_SIZE      512    /* physical sector size in bytes */
#define MAX_SIZE        1440    /* # sectors on a high-capacity (720K) disk */
#define NR_HEADS        0x02    /* two heads (i.e., two tracks/cylinder) */

#define MOTOR_OFF       3*HZ    /* how long to wait before stopping motor */

/* Error codes */
#define ERR_SEEK          -1    /* bad seek */
#define ERR_TRANSFER      -2    /* bad transfer */
#define ERR_STATUS        -3    /* something wrong when getting status */
#define ERR_RECALIBRATE   -4    /* recalibrate didn't work properly */
#define ERR_WR_PROTECT    -5    /* diskette is write protected */
#define ERR_DRIVE         -6    /* something wrong with a drive */

/* Miscellaneous. */
#define MOTOR_RUNNING   0xFF    /* message type for clock interrupt */
#define MAX_ERRORS        20    /* how often to try rd/wt before quitting */
#define NR_DRIVES          4    /* maximum number of drives */
#define NT                 3    /* number of diskette/drive combinations */

/* Variables. */
PRIVATE struct floppy {         /* main drive struct, one entry per drive */
  int fl_opcode;                /* DISK_READ or DISK_WRITE */
  int fl_curcyl;                /* current cylinder */
  int fl_procnr;                /* which proc wanted this operation? */
  int fl_drive;                 /* drive number addressed */
  int fl_cylinder;              /* cylinder number addressed */
  int fl_sector;                /* sector addressed */
  int fl_head;                  /* head number addressed */
  int fl_count;                 /* byte count */
  vir_bytes fl_address;         /* user virtual address */
  char fl_results;              /* the controller status */
  char fl_calibration;          /* CALIBRATED or UNCALIBRATED */
  char fl_density;              /* 0 = 360K/360K; 1 = 360K/720K; 2 = 720K/720K*/
} floppy[NR_DRIVES];

#define UNCALIBRATED       0    /* drive needs to be calibrated at next use */
#define CALIBRATED         1    /* no calibration needed */

PRIVATE int motor_status;       /* current motor status */
PRIVATE int initialized;        /* set to 1 after first successful transfer */
PRIVATE int d;                  /* diskette/drive combination */

PRIVATE message mess;           /* message buffer for in and out */

/* Ports for selecting drives and starting/stopping motor. */
PRIVATE int sel_drive[] = {0x10A, 0x108, 0x106, 0x104};
#define MOTOR_START  0x009E
#define MOTOR_STOP   0x009C
#define MOTOR_CTL    0x0080

PRIVATE char interleave[] = {1,2,3,4,5,6,7,8,9};

/* Three combinations of diskette/drive are supported:
 * # Drive  diskette  Sectors  Tracks  Rotation Type
 * 0  360K    360K      9       40     300 RPM  Standard PC DSDD
 * 1  720K    360K      9       40     300 RPM  Standard PC DSDD in quad drive
 * 2  720K    720K      9       80     300 RPM  Quad density PC compatible
 */
PRIVATE int gap[NT] =
       {0x2A, 0x2A, 0x2A}; /* gap size */
PRIVATE int nr_sectors[NT] =
       {9, 9, 9};          /* sectors/track */
PRIVATE int nr_blocks[NT] =
       {720, 720, 1440};   /* sectors/diskette*/
PRIVATE int steps_per_cyl[NT] =
       {1, 2, 1};          /* 2 = dbl step */
PRIVATE int mtr_setup[NT] =
       {HZ/4,HZ/4,HZ/4};   /* in ticks */

/*===========================================================================*
 *                              floppy_task                                  *
 *===========================================================================*/
PUBLIC floppy_task()
{
/* Main program of the floppy disk driver task. */

  int r, caller, proc_nr;

  floppy_init();

  /* Here is the main loop of the disk task.  It waits for a message, carries
   * it out, and sends a reply.
   */
  while (TRUE) {
        /* First wait for a request to read or write a disk block. */
        receive(ANY, &mess);    /* get a request to do some work */
        if (mess.m_source < 0)
          panic("disk task got message from ", mess.m_source);
        caller = mess.m_source;
        proc_nr = mess.PROC_NR;

        /* Now carry out the work. */
        switch(mess.m_type) {
            case DISK_READ:     r = do_rdwt(&mess);     break;
            case DISK_WRITE:    r = do_rdwt(&mess);     break;
            default:            r = EINVAL;             break;
        }

        /* Finally, prepare and send the reply message. */
        mess.m_type = TASK_REPLY;
        mess.REP_PROC_NR = proc_nr;
        mess.REP_STATUS = r;    /* # of bytes transferred or error code */
        send(caller, &mess);    /* send reply to caller */
  }
}


/*===========================================================================*
 *                              do_rdwt                                      *
 *===========================================================================*/
PRIVATE int do_rdwt(m_ptr)
message *m_ptr;                 /* pointer to read or write message */
{
/* Carry out a read or write request from the disk. */
  register struct floppy *fp;
  int r, drive, errors, stop_motor();
  long block;

  /* Decode the message parameters. */
  drive = m_ptr->DEVICE;
  if (drive < 0 || drive >= NR_DRIVES) return(EIO);
  fp = &floppy[drive];                  /* 'fp' points to entry for this drive*/
  fp->fl_drive = drive;                 /* save drive number explicitly */
  fp->fl_opcode = m_ptr->m_type;        /* DISK_READ or DISK_WRITE */
  if (m_ptr->POSITION % BLOCK_SIZE != 0) return(EINVAL);
  block = m_ptr->POSITION/SECTOR_SIZE;
  if (block >= MAX_SIZE) return(EOF);   /* sector is beyond end of disk */
  d = fp->fl_density;                   /* diskette/drive combination */
  fp->fl_cylinder = (int) (block / (NR_HEADS * nr_sectors[d]));
  fp->fl_sector = (int) interleave[block % nr_sectors[d]];
  fp->fl_head = (int) (block % (NR_HEADS*nr_sectors[d]))/nr_sectors[d];
  fp->fl_count = m_ptr->COUNT;
  fp->fl_address = (vir_bytes) m_ptr->ADDRESS;
  fp->fl_procnr = m_ptr->PROC_NR;
  if (fp->fl_count != BLOCK_SIZE) return(EINVAL);

  errors = 0;

  /* This loop allows a failed operation to be repeated. */
  while (errors <= MAX_ERRORS) {

        /* If a lot of errors occur when 'initialized' is 0, it probably
         * means that we are trying at the wrong density.  Try another one.
         * Increment 'errors' here since loop is aborted on error.
         */
        errors++;               /* increment count once per loop cycle */
        if (errors % (MAX_ERRORS/NT) == 0) {
                d = (d + 1) % NT;       /* try next density */
                fp->fl_density = d;
        }
        if (block >= nr_blocks[d]) continue;

        /* Now set up the DMA chip. */
        dma_setup(fp);

        /* See if motor is running; if not, turn it on and wait */
        start_motor();

        /* If we are going to a new cylinder, perform a seek. */
        r = seek(fp);
        if (r != OK) continue;  /* if error, try again */

        /* Perform the transfer. */
        r = transfer(fp);
        if (r == OK) break;     /* if successful, exit loop */
        if (r == ERR_WR_PROTECT) break; /* retries won't help */
  }

  /* Start watch_dog timer to turn motor off in a few seconds */
  clock_mess(MOTOR_OFF, stop_motor);
  if (r == OK && fp->fl_cylinder > 0) initialized = 1;  /* seek works */
  return(r == OK ? BLOCK_SIZE : EIO);
}

/*===========================================================================*
 *                              seek                                         *
 *===========================================================================*/
PRIVATE int seek(fp)
struct floppy *fp;              /* pointer to the drive struct */
{
/* Issue a SEEK command on the indicated drive unless the arm is already
 * positioned on the correct cylinder.
 */
  int stepidx;

  /* Have we ever used this drive? */
  if (fp->fl_calibration == UNCALIBRATED)
    if (recalibrate(fp) != OK) return(ERR_SEEK);
  /* Are we already on the correct cylinder? */
  if (fp->fl_curcyl == fp->fl_cylinder) return(OK);

  /* No.  Wrong cylinder.  Issue a SEEK and wait for interrupt. */
  port_out(sel_drive[fp->fl_drive], 1);
  for (stepidx = 1; stepidx < steps_per_cyl[d]; stepidx++) {
    port_out(FDC_DATA, fp->fl_cylinder);
    port_out(FDC_TRACK, fp->fl_curcyl);
    port_out(FDC_COMMAND, FDC_SEEK_NV);
    receive(HARDWARE, &mess);
     /* Interrupt has been received.  Check drive status. */
    fdc_results(fp);            /* get controller status */
    if (fp->fl_results & (SEEK_ERROR|CRC_ERROR|NOT_READY)) return(ERR_SEEK);
  }
  port_out(FDC_DATA, fp->fl_cylinder);
  port_out(FDC_TRACK, fp->fl_curcyl);
  port_out(FDC_COMMAND, FDC_SEEK);
  receive(HARDWARE, &mess);

  /* Interrupt has been received.  Check drive status. */
  fdc_results(fp);              /* get controller status */
  if (fp->fl_results & (SEEK_ERROR | CRC_ERROR | NOT_READY)) return(ERR_SEEK);
  fp->fl_curcyl = fp->fl_cylinder;
  return(OK);
}

/*===========================================================================*
 *                              transfer                                     *
 *===========================================================================*/
PRIVATE int transfer(fp)
register struct floppy *fp;     /* pointer to the drive struct */
{
/* The drive is now on the proper cylinder.  Read or write 1 block. */

  int op, dmacom;

  /* Never attempt a transfer if the drive is uncalibrated or motor is off. */
  if (fp->fl_calibration == UNCALIBRATED) return(ERR_TRANSFER);
  if (!motor_status) return(ERR_TRANSFER);

  word_in(DMA_COMMAND, &dmacom);
  dmacom |= DMA_START;
  word_out(DMA_COMMAND, dmacom);
  port_out(sel_drive[fp->fl_drive], 1);
  port_out(FDC_SECTOR, fp->fl_sector);
  op = (fp->fl_opcode == DISK_READ ? FDC_READ : FDC_WRITE);
  op |= (fp->fl_head << 1);
  port_out(FDC_COMMAND, op);
  receive(HARDWARE, &mess);
  fp->fl_sector += 1;
  if (fp->fl_sector > nr_sectors[d]) {
    fp->fl_sector = 1;
    fp->fl_head = 1;
  }
  port_out(FDC_SECTOR, fp->fl_sector);
  op = (fp->fl_opcode == DISK_READ ? FDC_READ : FDC_WRITE);
  op |= (fp->fl_head << 1);
  port_out(FDC_COMMAND, op);
  receive(HARDWARE, &mess);
  port_out(sel_drive[fp->fl_drive], 0);

  /* Get controller status and check for errors. */
  fdc_results(fp);
  if (fp->fl_results & NOT_FOUND) fp->fl_calibration = UNCALIBRATED;
  if (fp->fl_results & WRITE_PROTECT) {
    printf("Diskette in drive %d is write protected.\n", fp->fl_drive);
    return(ERR_WR_PROTECT);
  }
  if (fp->fl_results & (NOT_READY|CRC_ERROR|LOST_DATA)) return(ERR_TRANSFER);

  return(OK);
}

/*===========================================================================*
 *                              fdc_results                                  *
 *===========================================================================*/
PRIVATE fdc_results(fp)
register struct floppy *fp;     /* pointer to the drive struct */
{
/* Extract results from the controller after an operation. */

  int status;

  port_in(FDC_STATUS, &status);
  fp->fl_results = status & BYTE;
}

/*===========================================================================*
 *                              recalibrate                                  *
 *===========================================================================*/
PRIVATE int recalibrate(fp)
register struct floppy *fp;     /* pointer to the drive struct */
{
/* The floppy disk controller has no way of determining its absolute arm
 * position (cylinder).  Instead, it steps the arm a cylinder at a time and
 * keeps track of where it thinks it is (in software).  However, after a
 * SEEK, the hardware reads information from the diskette telling where the
 * arm actually is.  If the arm is in the wrong place, a recalibration is done,
 * which forces the arm to cylinder 0.  This way the controller can get back
 * into sync with reality.
 */

  /* Issue the RECALIBRATE command and wait for the interrupt. */
  start_motor(fp);              /* can't recalibrate with motor off */
  port_out(sel_drive[fp->fl_drive], 1); /* select drive */
  port_out(FDC_COMMAND, FDC_RECALIBRATE); /* tell controller to RESTORE */
  receive(HARDWARE, &mess);     /* wait for interrupt message */
  port_out(sel_drive[fp->fl_drive], 0); /* deselect drive */

  /* Determine if the recalibration succeeded. */

  fdc_results(fp);              /* get results of the command */
  fp->fl_curcyl = -1;           /* force a SEEK next time */
  if (fp->fl_results & (SEEK_ERROR | CRC_ERROR | NOT_READY)) {
    /* Recalibration failed. */
    fp->fl_calibration = UNCALIBRATED;
    return(ERR_RECALIBRATE);
  } else {
    /* Recalibration succeeded. */
    fp->fl_calibration = CALIBRATED;
    fp->fl_curcyl = 0;
    return(OK);
  }
}

/*===========================================================================*
 *                              dma_setup                                    *
 *===========================================================================*/
PRIVATE dma_setup(fp)
struct floppy *fp;              /* pointer to the drive struct */
{
  int mode, src_low, src_high, count, floppy_low = 6, floppy_high = 0, reading;
  int dst_low, dst_high;
  vir_bytes vir, ct;
  phys_bytes user_phys;
  extern phys_bytes umap();

  reading = (fp->fl_opcode == DISK_READ ? 1 : 0);
  mode = (reading ? DMA_READ : DMA_WRITE);
  vir = (vir_bytes) fp->fl_address;
  ct = (vir_bytes) fp->fl_count;
  user_phys = umap(proc_addr(fp->fl_procnr), D, vir, ct);

  /*
   * Check for bad buffer address.  This error means FS contains a bug.
   */
  if (user_phys == 0) panic("FS gave floppy disk driver bad addr", (int) vir);

  if (reading) {
    src_low = floppy_low;
    src_high = floppy_high;
    dst_low  = (int) (user_phys >>  0) & 0xFFFF;
    dst_high = (int) (user_phys >> 16) & 0xFFFF;
  }
  else {
    src_low  = (int) (user_phys >>  0) & 0xFFFF;
    src_high = (int) (user_phys >> 16) & 0xFFFF;
    dst_low = floppy_low;
    dst_high = floppy_high;
  }
  count = (int) ct;

  /* Now set up the DMA registers. */
  lock();
  word_out(DMA_SRC_LOW,  src_low);
  word_out(DMA_SRC_HIGH, src_high);
  word_out(DMA_DST_LOW,  dst_low);
  word_out(DMA_DST_HIGH, dst_high);
  word_out(DMA_LENGTH,   count);
  word_out(DMA_COMMAND,  mode);
  unlock();
}

/*===========================================================================*
 *                              start_motor                                  *
 *===========================================================================*/
PRIVATE start_motor()
{
/* Control of the floppy disk motors is a big pain.  If a motor is off, you
 * have to turn it on first, which takes 1/2 second.  You can't leave it on
 * all the time, since that would wear out the diskette.  However, if you turn
 * the motor off after each operation, the system performance will be awful.
 * The compromise used here is to leave it on for a few seconds after each
 * operation.  If a new operation is started in that interval, it need not be
 * turned on again.  If no new operation is started, a timer goes off and the
 * motor is turned off. Interrupts must be disabled temporarily to prevent
 * clock interrupt from turning off motors while we are testing the bits.
 */

  int running, send_mess();

  lock();                       /* no interrupts while checking out motor */
  running = motor_status;       /* nonzero if motor is running */
  port_out(MOTOR_START, MOTOR_CTL);
  motor_status = 1;
  unlock();

  /* If the motor was already running, we don't have to wait for it. */
  if (running) return;                  /* motor was already running */
  clock_mess(mtr_setup[d], send_mess);  /* motor was not running */
  receive(CLOCK, &mess);                /* wait for clock interrupt */
}

/*===========================================================================*
 *                              stop_motor                                   *
 *===========================================================================*/
PRIVATE stop_motor()
{
/* This routine is called by the clock interrupt after several seconds have
 * elapsed with no floppy disk activity.
 */

  port_out(MOTOR_STOP, MOTOR_CTL);
  motor_status = 0;
}

/*===========================================================================*
 *                              clock_mess                                   *
 *===========================================================================*/
PRIVATE clock_mess(ticks, func)
int ticks;                      /* how many clock ticks to wait */
int (*func)();                  /* function to call upon time out */
{
/* Send the clock task a message. */

  mess.m_type = SET_ALARM;
  mess.CLOCK_PROC_NR = FLOPPY;
  mess.DELTA_TICKS = ticks;
  mess.FUNC_TO_CALL = func;
  sendrec(CLOCK, &mess);
}

/*===========================================================================*
 *                              send_mess                                    *
 *===========================================================================*/
PRIVATE send_mess()
{
/* This routine is called when the clock task has timed out on motor startup.*/

  mess.m_type = MOTOR_RUNNING;
  send(FLOPPY, &mess);
}

/*===========================================================================*
 *                              floppy_init                                  *
 *===========================================================================*/
PRIVATE floppy_init()
{
  /* 0x10C is 5 1/4" drive selection port */
  /* 0x10E is double density selection (active low) */

  port_out(0x10C, 1);
  port_out(0x10E, 0);
}
---------------------------------END OF FILE---------------------------------

          -Paul Dickson
            Dickson%pco @ BCO-MULTICS.HBI.HONEYWELL.COM