[comp.os.minix] SCSI disk driver for PC-230.

cagney@chook.ua.oz (Andrew Cagney - aka Noid) (05/29/89)

This is a SCSI driver for PC minix on a Micro Byte PC 230. The PC 230 uses
a WD 33C93 SCSI controller. Obiviously this driver will only work on a PC 230.
If you are writing a SCSI driver for another pc with the same
controller chip set then this will make a very good starting point.
If you are interested in how simple it is to drive a SCSI port then the code
below is again interesting.

Features: This controller assumes that the PC230 is at SCSI address 0 and the
          hard disk is at SCSI address 1. This will be changed when I or Martin
          Standiford get the time.

Notes: This will not link with 8 character linkers (eg the one on minix) as
       the two identifiers w_scsi_dma_setup and w_scsi_cdb_setup are identical
       in the first 8 characters. If you change these names then it should
       work.

Finally Martin Stanford (of micro byte) was the author, he should receive the
credit, not me.

                                                Andrew Cagney
                                                cagney@cs.ua.oz.au

-------------------------------------------------------------------------
static w_do_rdwt(),w_scsi_dma_setup(),w_wd3393_setup(),w_scsi_cdb_setup(),
       w_transfer(),win_results(),w_wait_int(),init_params(),copy_prt();
/***********************************************************************/
/* This file contains a driver for the WD 33C93 SCSI controller
 * chip used in the Micro Byte Systems PC230.
 *
 * Original ST506 code written by Adri Koppes.
 * Extreme modification by Martin Sandiford for SCSI controller.
 *
 * 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:
 *
 *	 winchester_task:	main entry when system is brought up
 *
 */

#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 "proc.h"

#define DEBUG	       FALSE	/* TRUE: enable debug messages */
#define MONITOR		TRUE	/* TRUE: monitor performance of busy loops */
#define MAX_DRIVES         1

/* I/O Ports used by SCSI disk task. */
PRIVATE int _scsiix =	       0x0E8;	/* SCSI controller index port */
PRIVATE int _scsida =	       0x0E9;	/* SCSI controller data port */

#define	INTDMA3	       0x0E6		/* PC230 Set DMA3/IRQ5 internal */

#define DMA_ADDR       0x006	/* port for low 16 bits of DMA address */
#define DMA_COUNT      0x007	/* port for DMA count (count =	bytes - 1) */
#define DMA_STATUS     0x008
#define DMA_INIT       0x00A	/* DMA init port */
#define DMA_M1	       0x00B	/* DMA status port */
#define DMA_M2	       0x00C	/* DMA status port */
#define DMA_TOP	       0x082	/* port for top 4 bits of 20-bit DMA addr */

/* SCSI unit command bytes. */
#define SCSI_READ	0x08	/* command for the drive to read */
#define SCSI_WRITE	0x0A	/* command for the drive to write */

/* SCSI controller indexes */
#define SCSI_CTRL	0x01
#define SCSI_TIMEO	0x02
#define SCSI_CDB1	0x03	/* Command Descriptor Block (1st byte) */
#define SCSI_TLUN	0x0F	/* Target LUN (always 0 on PC230) */
#define	SCSI_TCMSB	0x12	/* Transfer count MSB etc. */
#define	SCSI_TCNSB	0x13
#define	SCSI_TCLSB	0x14
#define SCSI_DESTID	0x15
#define SCSI_SOURCEID	0x16  
#define	SCSI_STATUS	0x17	/* Status of command after completion interrupt */
#define SCSI_CMND	0x18	/* WD3393 command register */

/* WD33C93 commands */
#define SCSI_SELATNS	0x08	/* Select with atn and transfer cmd for chip */

/* SCSI controller status bits */
#define	SST_CIP		0x10	/* Command in progress bit */
#define	SST_BUSY	0x20	/* Controller busy bit */
#define SST_INTERRUPT	0x80	/* Command complete interrupt happened bit */

/* DMA channel commands. */
#define DMA_READ	0x47	/* DMA read opcode */
#define DMA_WRITE	0x4B	/* DMA write opcode */

/* Parameters for the disk drive. */
#define SECTOR_SIZE	 512	/* physical sector size in bytes */

/* Error codes */
#define ERR		  -1	/* general error */

/* Miscellaneous. */
#define MAX_ERRORS	   4	/* how often to try rd/wt before quitting */
#define MAX_RESULTS	   4	/* max number of bytes controller returns */
#define NR_DEVICES	  10	/* maximum number of drives */
#define MAX_RETRY  32000	/* max # times to try to output to WIN */
#define PART_TABLE     0x1C6	/* IBM partition table starts here in sect 0 */
#define DEV_PER_DRIVE	   5	/* hd0 + hd1 + hd2 + hd3 + hd4 = 5 */

/* Variables. */
PRIVATE struct wini {		/* main drive struct, one entry per drive */
  int wn_opcode;		/* DISK_READ or DISK_WRITE */
  int wn_procnr;		/* which proc wanted this operation? */
  int wn_drive;			/* drive number addressed (<< 5) */
  long wn_secaddr;		/* Sector address of req'd sector */
  long wn_low;			/* lowest cylinder of partition */
  long wn_size;			/* size of partition in blocks */
  int wn_count;			/* byte count */
  vir_bytes wn_address;		/* user virtual address */
  char wn_results[MAX_RESULTS];	/* the controller can give lots of output */
} wini[NR_DEVICES];

PRIVATE int nr_drives;		 /* Number of drives */

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

PRIVATE char command[6];		/* Common command block */

PRIVATE unsigned char buf[BLOCK_SIZE]; /* Buffer used by the startup routine */

/*=========================================================================*
 *				winchester_task				   * 
 *=========================================================================*/
PUBLIC winchester_task()
{
/* Main program of the winchester disk driver task. */

  int r, caller, proc_nr;

  /* First initialize the controller */
  init_params();

  /* 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, &w_mess);	/* get a request to do some work */
	if (w_mess.m_source < 0) {
		printf("winchester task got message from %d ", w_mess.m_source);
		continue;
	}
	caller = w_mess.m_source;
	proc_nr = w_mess.PROC_NR;

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

	/* Finally, prepare and send the reply message. */
	w_mess.m_type = TASK_REPLY;
	w_mess.REP_PROC_NR = proc_nr;

	w_mess.REP_STATUS = r;	/* # of bytes transferred or error code */
	send(caller, &w_mess);	/* send reply to caller */
  }
}


/*==========================================================================*
 *				w_do_rdwt				    * 
 *==========================================================================*/
PRIVATE int w_do_rdwt(m_ptr)
message *m_ptr;			/* pointer to read or write w_message */
{
/* Carry out a read or write request from the disk. */
  register struct wini *wn;
  int r, device, errors = 0;
  long sector;

  /* Decode the w_message parameters. */
  device = m_ptr->DEVICE;
  if (device < 0 || device >= NR_DEVICES)
	return(EIO);
  if (m_ptr->COUNT != BLOCK_SIZE)
	return(EINVAL);

  wn = &wini[device];		/* 'wn' points to entry for this drive */

  wn->wn_opcode = m_ptr->m_type;	/* DISK_READ or DISK_WRITE */
  if (m_ptr->POSITION % BLOCK_SIZE != 0)
	return(EINVAL);

  sector = m_ptr->POSITION/SECTOR_SIZE;
  if ((sector+BLOCK_SIZE/SECTOR_SIZE) > wn->wn_size)
	return(EOF);

  sector += wn->wn_low;
  wn->wn_secaddr = sector;
  wn->wn_count = m_ptr->COUNT;
  wn->wn_address = (vir_bytes) m_ptr->ADDRESS;
  wn->wn_procnr = m_ptr->PROC_NR;

  /* This loop allows a failed operation to be repeated. */
  while (errors <= MAX_ERRORS) {
	errors++;		/* increment count once per loop cycle */
	if (errors >= MAX_ERRORS)
		return(EIO);

	/* Setup WD33C93 with drive information */
	w_wd3393_setup(wn);

	/* Setup SCSI CDB bytes */
	w_scsi_cdb_setup(wn);

	/* Setup SCSI transfer count and DMA addr/len */
	w_scsi_dma_setup(wn);

	/* Do the transfer */
	r = w_transfer();

	if (r == OK) break;	/* if successful, exit loop */

  }

  return(r == OK ? BLOCK_SIZE : EIO);
}

/*==========================================================================*
 *				w_wd3393_setup				    * 
 *==========================================================================*/
PRIVATE w_wd3393_setup(wn)
register struct wini *wn;
{
  /* wn is not used as yet, but it may be necessary in future to send the
     correct destination ID. Currently this is hard wired to 1. */
  int old_state;
  
  old_state = lock();
  port_out(_scsiix, SCSI_SOURCEID);
  port_out(_scsida, 0);

  port_out(_scsiix, SCSI_CTRL);
  port_out(_scsida, 0x08);

  port_out(_scsiix, SCSI_TIMEO);
  port_out(_scsida, 12);

  port_out(_scsiix, SCSI_DESTID);
  port_out(_scsida, 1);

  port_out(_scsiix, SCSI_TLUN);
  port_out(_scsida, 0);
  restore(old_state);
}

/*==========================================================================*
 *				w_scsi_cdb_setup			    * 
 *==========================================================================*/
PRIVATE w_scsi_cdb_setup(wn)
register struct wini *wn;
{
  int old_state, i;
  /* The command is issued by outputing 6 bytes to the controller chip. */
  command[0] = (wn->wn_opcode == DISK_READ ? SCSI_READ : SCSI_WRITE);
  command[1] = (wn->wn_secaddr >> 16) & 0x1F;
  command[2] = (wn->wn_secaddr >> 8)  & 0xFF;
  command[3] = wn->wn_secaddr         & 0xFF;
  command[4] = BLOCK_SIZE/SECTOR_SIZE;
  command[5] = 0;
  old_state = lock();
  for (i = 0; i < 6; i++)
  {
	port_out(_scsiix, SCSI_CDB1+i);
	port_out(_scsida, command[i]);
  }
  port_out(_scsiix, SCSI_CTRL);
  port_out(_scsida, 0x88);
  restore(old_state);
}

/*==========================================================================*
 *				w_dma_scsi_setup			    * 
 *==========================================================================*/
PRIVATE w_scsi_dma_setup(wn)
register struct wini *wn;		/* pointer to the drive struct */
{
/* The IBM PC can perform DMA operations by using the DMA chip.	 To use it,
 * the DMA (Direct Memory Access) chip is loaded with the 20-bit memory address
 * to by read from or written to, the byte count minus 1, and a read or write
 * opcode.	This routine sets up the DMA chip.	Note that the chip is not
 * capable of doing a DMA across a 64K boundary (e.g., you can't read a 
 * 512-byte block starting at physical address 65520).
 */

  int mode, low_addr, high_addr, top_addr, low_ct, high_ct, top_end, old_state;
  int low_ct1, high_ct1;
  vir_bytes vir, ct;
  phys_bytes user_phys;
  extern phys_bytes umap();

  mode = (wn->wn_opcode == DISK_READ ? DMA_READ : DMA_WRITE);
  vir = (vir_bytes) wn->wn_address;
  ct = (vir_bytes) wn->wn_count;
  user_phys = umap(proc_addr(wn->wn_procnr), D, vir, ct);
  low_addr	= (int) user_phys & BYTE;
  high_addr = (int) (user_phys >>  8) & BYTE;
  top_addr	= (int) (user_phys >> 16) & BYTE;

  low_ct  = (int) (ct - 1) & BYTE;
  high_ct = (int) ( (ct - 1) >> 8) & BYTE;
  low_ct1  = (int) ct & BYTE;
  high_ct1 = (int) (ct >> 8) & BYTE;

  /* Check to see if the transfer will require the DMA address counter to
   * go from one 64K segment to another.  If so, do not even start it, since
   * the hardware does not carry from bit 15 to bit 16 of the DMA address.
   * Also check for bad buffer address.	 These errors mean FS contains a bug.
   */
  if (user_phys == 0)
	  panic("FS gave winchester disk driver bad addr", (int) vir);
  top_end = (int) (((user_phys + ct - 1) >> 16) & BYTE);
  if (top_end != top_addr) panic("Trying to DMA across 64K boundary", top_addr);

  /* Now set up the DMA registers. */
  old_state = lock();

  port_out(_scsiix, SCSI_TCMSB);
  port_out(_scsida, 0);

  port_out(_scsiix, SCSI_TCNSB);
  port_out(_scsida, high_ct1);

  port_out(_scsiix, SCSI_TCLSB);
  port_out(_scsida, low_ct1);

  port_out(DMA_M1, mode);	/* send command */
  port_out(DMA_M2, mode);	/* clear flip/flop */
  port_out(DMA_TOP, top_addr);	/* output highest 4 bits */
  port_out(DMA_ADDR, low_addr);	/* output low-order 8 bits */
  port_out(DMA_ADDR, high_addr);/* output next 8 bits */
  port_out(DMA_COUNT, low_ct);	/* output low 8 bits of count - 1 */
  port_out(DMA_COUNT, high_ct);	/* output high 8 bits of count - 1 */

  restore(old_state);
}

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

  int old_state;

  port_out(DMA_INIT, 3);	/* initialize DMA */
  port_in(DMA_STATUS, &old_state);
  old_state = lock();
  port_out(_scsiix, SCSI_CMND);
  port_out(_scsida, SCSI_SELATNS);
  restore(old_state);

  /* Block, waiting for disk interrupt. */
  w_wait_int();

  /* Get controller status and check for errors. */
  if (win_results(wn) == OK)
	return(OK);
  return(ERR);
}


/*===========================================================================*
 *				win_results				     * 
 *===========================================================================*/
PRIVATE int win_results(wn)
register struct wini *wn;	/* pointer to the drive struct */
{
/* Extract results from the controller after an operation. */
  int status;

  port_out(_scsiix, SCSI_STATUS);
  port_in(_scsida, &status);

  if (!(status & 0xE0))		/* Test "error" bits */
	return(OK);

  return(ERR);
}

/*===========================================================================*
 *				w_wait_int				     *
 *===========================================================================*/
PRIVATE w_wait_int()
{
   int r, i; /* Some local storage */

   receive(HARDWARE, &w_mess);

   for(i=0; i<MAX_RETRY; ++i) {
	port_in(_scsiix, &r);
	if(r & SST_INTERRUPT)
		break;		/* Exit if end of int */
  }

#if	 MONITOR
   if(i > 10) {		/* Some arbitrary limit below which we don't really care */
	if(i == MAX_RETRY)
		printf("wini: timeout waiting for INTERRUPT status\n");
	else
		printf("wini: %d loops waiting for INTERRUPT status\n", i);
   }
#endif	/* MONITOR */
}


/*============================================================================*
 *				init_params				      *
 *===========================================================================*/
PRIVATE init_params()
{
/* This routine is called at startup to initialize the partition table,
 * the number of drives and the controller
*/
  unsigned int i;
  extern phys_bytes umap();

  /* Set DMA channel 3 and interrupt 5 to internal */
  port_out(INTDMA3, 0);

  /* Get the number of drives from the bios */
  phys_copy(0x475L, umap(proc_addr(WINCHESTER), D, buf, 1), 1L);
  nr_drives = (int) *buf > MAX_DRIVES ? MAX_DRIVES : (int) *buf;

  /* Get the address of the WD33C93 SCSI chip */
  phys_copy(0x48CL, umap(proc_addr(WINCHESTER), D, buf, 1), 2L);
  _scsiix = *buf + (buf[1] << 8);
  _scsida = _scsiix+1;

  /* Set the parameters in the drive structure */
  for (i = 0; i < DEV_PER_DRIVE; i++) {
	wini[i].wn_drive = 0 << 5;	/* Set drive number */
  }

  wini[0].wn_low = wini[DEV_PER_DRIVE].wn_low = 0L;

  /* You may wish to modify this to reflect the actual number of
     cyls, hds, spt */
  wini[0].wn_size = 603L*17L*4L;

  for (i = DEV_PER_DRIVE; i < (2*DEV_PER_DRIVE); i++) {
	wini[i].wn_drive = 1 << 5;	/* Set drive number */
  }

  wini[DEV_PER_DRIVE].wn_size = 603L * 8L * 34L;

  /* Read the partition table for each drive and save them */
  for (i = 0; i < nr_drives; i++) {
	w_mess.DEVICE = i * DEV_PER_DRIVE;
	w_mess.POSITION = 0L;
	w_mess.COUNT = BLOCK_SIZE;
	w_mess.ADDRESS = (char *) buf;
	w_mess.PROC_NR = WINCHESTER;
	w_mess.m_type = DISK_READ;
	if (w_do_rdwt(&w_mess) != BLOCK_SIZE) {
		printf("Can't read partition table of winchester %d ", i);
		continue;
	}
	copy_prt(i * DEV_PER_DRIVE);
  }
}

/*==========================================================================*
 *				copy_prt				    *
 *==========================================================================*/
PRIVATE copy_prt(drive)
int drive;
{
/* This routine copies the partition table for the selected drive to
 * the variables wn_low and wn_size
 */

  register int i, offset;
  struct wini *wn;
  long adjust;

  for (i=0; i<4; i++) {
	adjust = 0;
	wn = &wini[i + drive + 1];
	offset = PART_TABLE + i * 0x10;
	wn->wn_low = *(long *)&buf[offset];
	if ((wn->wn_low % (BLOCK_SIZE/SECTOR_SIZE)) != 0) {
		adjust = wn->wn_low;
		wn->wn_low = (wn->wn_low/(BLOCK_SIZE/SECTOR_SIZE)+1)*(BLOCK_SIZE/SECTOR_SIZE);
		adjust = wn->wn_low - adjust;
	}
	wn->wn_size = *(long *)&buf[offset + sizeof(long)] - adjust;
  }
  sort(&wini[drive + 1]);
}

sort(wn)
register struct wini *wn;
{
  register int i,j;

  for (i=0; i<4; i++)
	for (j=0; j<3; j++)
		if ((wn[j].wn_low == 0) && (wn[j+1].wn_low != 0))
			swap(&wn[j], &wn[j+1]);
		else if (wn[j].wn_low > wn[j+1].wn_low && wn[j+1].wn_low != 0)
			swap(&wn[j], &wn[j+1]);
}

swap(first, second)
register struct wini *first, *second;
{
  register struct wini tmp;

  tmp = *first;
  *first = *second;
  *second = tmp;
}

/* end of the file 230_wini.c */