/*
 *   10MB and 2.5MB disc driver
 *	(C) Ross Nealon, 1982
 */

#include	"../h/param.h"
#include	"../h/buf.h"
#include	"../h/conf.h"
#include	"../h/selch.h"
#include	"../h/systm.h"
#include	"../h/inf.h"

/*
 *   Disc drive status and commands
 */
#define		WRTPROT		0x80
#define		WRTCHK		0x40
#define		ILLADDR		0x20
#define		INTERLOCK	0x10
#define		NRSRW		0x08
#define		SEEKINC		0x02
#define		OFFLINE		0x01
#define		DSK_UNRCV	(WRTCHK | ILLADDR | SEEKINC | NRSRW)

#define		SEEK		0x02
#define		RESTORE		0x01

/*
 *   Disc controller status and commands
 */
#define		OVERRUN		0x80
#define		HDFAIL		0x40
#define		DEFTRK		0x20
#define		CYL_OV		0x10
#define		IDLE		0x02
#define		DATAERR		0x01
#define		CNTL_UNRCV	(OVERRUN | HDFAIL | DEFTRK | DATAERR)

#define		RESET		0x08
#define		WRITE		0x02
#define		READ		0x01


/*
 *   Macros
 */
#define		CNTLIDLE(c)	while ((ss(c) & IDLE) == 0)
#define		INTLK(a)	while (ss(a) & INTERLOCK)
#define		DRIVENUMBER(d)	(minor(d) >> 2)
#define		PLATTER(d)	(minor(d) & 01)
#define		DUALDISC(d)	(minor(d) & 02)

#define		NCYL(i)		(dsktncyl[i->inf_type])
#define		NBPC(i)		(24)
#define		NBPT(i)		(12)
#define		ADDR(i)		(i->inf_flags & LOWER  ?  i->inf_addr+1 : i->inf_addr)

/*
 *   Offset to add to the device address to obtain the correct platter.
 *	- Also represents platter number from minor device number.
 */
#define		REMOVABLE	0		/* top platter */
#define		FIXED		1		/* bottom platter */

/*
 *   Simulated Dual Platter Logic States.
 *	- These states represent current I/O access states.
 *	- Codes should be > 0xff.
 */
#define		DUAL		0x0100		/* dual platter device */
#define		LOWER		0x0200		/* on lower platter */

/*
 *   Tables
 */
extern		int	ndsk;			/* number of discs */
extern	struct	buf	dsktab;			/* devtab, in bdevsw.d_tab */
extern	struct	inf	dskinf[ ];		/* info table */
extern		int	dskstart();		/* I/O startup routine */
extern		int	dsksintr();		/* channel interrupt routine */
extern		int	dsktimeout();		/* time out handler */
extern		int	dsknrdy();		/* re-start routine */

static	int	dsktncyl[ ]	=	{	/* highest cylinder per device */
		0,				/* illegal type */
		203,				/* 2.5 megabyte disc */
		408,				/* 10  megabyte disc */
		};


/*
 *   Disc strategy routine.
 */
dskstrategy(bp)
register struct buf *bp;
{
	register struct inf *inf;
	register int s;

	bp->b_resid = bp->b_bcount;
	if (DRIVENUMBER(bp->b_dev) >= ndsk)  {
		bp->b_flags |= B_ERROR;
		iodone(bp);
		return;
		}

	/*
	 *   Check block numbers
	 *	- Check start and end for within range
	 *	- Too high - looks like EOF for raw read, error otherwise
	 */
	inf = &dskinf[DRIVENUMBER(bp->b_dev)];
	if (bp->b_blkno >= NCYL(inf)*NBPC(inf))  {
		if ((bp->b_flags & (B_PHYS|B_READ)) != (B_PHYS|B_READ))
			bp->b_flags |= B_ERROR;
		iodone(bp);
		return;
		}

	if (inf->inf_dn >= 0)  {
		/* Monitoring */
		dk_numb[inf->inf_dn]++;
		dk_wds[inf->inf_dn] += (bp->b_bcount>>6);
		}

	/*
	 *   Queue I/O request.
	 */
	bp->av_forw = 0;
	s = spl5();
	if (!inf->inf_actf)
		inf->inf_actf = bp;
	else
		inf->inf_actl->av_forw = bp;

	inf->inf_actl = bp;
	splx(s);

	dsksync(inf, C_IDLE);
	return;
}


/*
 *   Synchronization routine.
 *	- Ensure only one `driver' is operative at a time.
 */
dsksync(inf, again)
register struct inf *inf;
register again;
{
	register int nc;
	register state;
	register s;

	nc = inf->inf_ncntl;
	s = spl5();
	state = cntltab[nc].c_state;
	if (state == C_IDLE)
		cntltab[nc].c_state = C_BUSY;
	if (state == C_BUSY  &&  again == C_AGAIN)
		cntltab[nc].c_state = C_AGAIN;
	splx(s);

	if (state != C_IDLE)  {
		trace(0x8000, "dskbusy", state);
		return;
		}

	/* Controller idle - start up I/O driver */
	Selchreq(inf, &dskstart, &dsksintr);
	return;
}


/*
 *   I/O start routine.
 *	- When called, the channel must be allocated to controller
 *	  infst->inf_cntl, and the controller must not be active,
 *	  though discs may be seeking.
 */
dskstart(infst)
register struct inf *infst;
{
	register struct inf *inf;
	register struct inf *infio;
	register int nc;
	register int i;
	register state;
	register s;

	trace(0x4000, "dskstart", infst);
	nc = infst->inf_ncntl;

	/*
	 *   Start as many seeks on controller `nc' as possible.
	 *	- Start seeks & I/O with infst last to be checked.
	 *	- Record first I/O candidate.
	 */
	infio = 0;
	inf = infst;
	for (i=0;  i<ndsk;  i++)  {
		inf++;
		if (inf >= &dskinf[ndsk])
			inf = &dskinf[0];

		if (nc != inf->inf_ncntl)
			/* Not this controller */
			continue;

		while (inf->inf_active == DNACT  &&  inf->inf_actf)  {
			/* Disc idle, but I/O queued */
			if (!dsksetup(inf))  {
				/* I/O would have failed */
				dskiodone(inf, B_ERROR);
				continue;
				}

			/* I/O checked and setup ok */
			break;
			}

		if (inf->inf_active == WSTART)  {
			/* Start up a seek */
			if (inf->inf_ccyl == inf->inf_cyl)  {
				/* Already there - try to start I/O */
				inf->inf_active = WSIO;
				}
			else  {
				/* Seek required */
				dskseek(inf);
				continue;
				}
			}

		if (inf->inf_active == WSIO  &&  !infio)  {
			/* Encountered first I/O request waiting */
			infio = inf;
			continue;
			}
		}

	/*
	 *   All possible seeks have been started.
	 *	- Check for and start one I/O if one is ready.
	 */
	if (infio)  {
		/* At least one I/O waiting */
		cntltab[nc].c_inf = infio;
		dskio(infio);
		return;
		}

	/*
	 *   No I/O has been started, free the channel and controller.
	 */
	Selchfree(infst);
	s = spl5();
	state = cntltab[nc].c_state;
	cntltab[nc].c_inf = 0;
	cntltab[nc].c_state = C_IDLE;
	splx(s);
	if (state == C_AGAIN)
		/* Something happened during dskstart */
		dsksync(infst, C_IDLE);

	return;
}


/*
 *   Set up for next i/o operation.
 *	- Set up drive address, cylinder/head/sector address.
 *	- Set up memory start & end addresses.
 *	- Check that all this is ok.
 */
dsksetup(inf)
register struct inf *inf;
{
	register struct buf *bp;
	register long bn;
	register int cyl;
	register dev_t dev;
	register int sz;

	bp = inf->inf_actf;
	dev = bp->b_dev;
	bn = bp->b_blkno;
	sz = bp->b_bcount / BSIZE;
	if (bp->b_bcount % BSIZE)
		sz++;

	cyl = bn / NBPC(inf);
	inf->inf_sector = (bn % NBPT(inf)) << 1;
	inf->inf_head   = ((bn % NBPC(inf)) < NBPT(inf)  ?  0 : 1);
	if (DUALDISC(dev))  {
		/* Simulate dual platter device for this I/O */
		if (PLATTER(dev) == FIXED)
			return(0);
		if (inf->inf_type == 1)
			/* 2.5 MB dev, dual platter logic not useable */
			return(0);

		inf->inf_flags |= DUAL;
		if (cyl & 01)
			/* Use lower platter */
			inf->inf_flags |= LOWER;

		cyl >>= 1;
		}
	else  {
		if (PLATTER(dev) == FIXED)
			inf->inf_flags |= LOWER;
		}

	if (cyl > NCYL(inf))
		return(0);


	if ((inf->inf_actf->b_flags & (B_READ|B_WRITE)) == B_WRITE)
		/* Write attempted */
		if (ss(ADDR(inf)) & WRTPROT)  {
			/* Disc protected - would have failed */
			deverror(inf->inf_actf, WRTPROT, ADDR(inf));
			return(0);
			}


	inf->inf_cyl	= cyl;
	inf->inf_sad	= bp->b_un.b_addr;
	inf->inf_ead	= bp->b_un.b_addr + bp->b_bcount - 1;
	inf->inf_fad	= 0;
	inf->inf_nblk	= sz;
	inf->inf_errcnt = 0;
	inf->inf_active	= WSTART;
	return(1);
}

/*
 *   Initiate seek.
 *	- Controller must be idle when this routine is called.
 */
dskseek(inf)
register struct inf *inf;
{
	register int cyl;
	register addr, cntl;
	register stat;

	addr = ADDR(inf);
	cntl = inf->inf_cntl;
	cyl  = inf->inf_cyl;
	trace(0x8000, "seek", addr);
	trace(0x8000, "cyl", cyl);

	INTLK(addr);
	stat = ss(addr);
	if (stat & OFFLINE)  {
		/* Arrange to try again in 10 seconds */
		inf->inf_active = DNRDY;
		timeout(&dsknrdy, (caddr_t) inf, 1000);
		return;
		}

	if (stat & DSK_UNRCV)  {
		/* Can't seek - log error, clean up, and continue */
		inf->inf_errcnt = NRETRY;
		deverror(inf->inf_actf, stat, addr);
		dskiodone(inf);
		dskrelease(inf);
		dsksync(inf, C_AGAIN);
		return;
		}

	if (inf->inf_dn >= 0)
		/* Monitoring */
		dk_busy |= (1 << inf->inf_dn);

	trace(0x8000, "stat", stat);
	inf->inf_active = WSEEK;
	timer(inf, 3, &dsktimeout, inf);
	inf->inf_last   = inf->inf_ccyl;
	inf->inf_ccyl   = SEEKING;

	wh(addr, cyl);
	oc(addr, SEEK|ENABLE);
}


/*
 *   Initiate I/O after seek complete
 */
dskio(inf)
register struct inf *inf;
{
	register addr, cntl, selch;
	register s;

	trace(0x4000, "dskio", inf);
	addr  = ADDR(inf);
	cntl  = inf->inf_cntl;
	selch = inf->inf_selch;
	oc(selch, STOP);
	INTLK(addr);

	trace(0x4000, "sad", inf->inf_sad);
	trace(0x4000, "ead", inf->inf_ead);
	wdh(selch, inf->inf_sad);
	wdh(selch, inf->inf_ead);
	wh(addr, inf->inf_cyl);
	wd(cntl, (inf->inf_head << 5) | inf->inf_sector);

	s = spl5();
	if (inf->inf_actf->b_flags & B_READ)  {
		oc(cntl,  READ|ENABLE);
		oc(selch, READ_GO);
		}
	else  {
		oc(cntl,  WRITE|ENABLE);
		oc(selch, GO);
		}

	inf->inf_active = WIO;
	timer(inf, 3, &dsktimeout, inf);
	splx(s);
	return;
}


/*
 *   Disc interrupt routine - interrupt occurs after
 *	- A seek completes.
 *	- Restore command completes.
 *	- Drive goes into a funny state (Interrupt only sometimes).
 */
dskdintr(dev, stat)
{
	register struct inf *inf;
	register addr;

	inf  = &dskinf[dev];
	addr = inf->inf_addr;
	trace(0x8000, "interrupt", addr);
	trace(0x8000, "status", stat);

	if (!inf->inf_actf)  {
		/* Unexpected interrupt */
		stat = ss(addr);
		if (!(stat & (OFFLINE|NRSRW)))
			printf("dsk %x interrupted %x\n", addr, stat);

		inf->inf_active = DNACT;
		return;
		}

	switch (inf->inf_active)  {

			/* Waiting for seek to complete */
		case WSEEK:
			timer(inf, 0);
			inf->inf_ccyl = inf->inf_cyl;
			inf->inf_active = WSIO;
			dsksync(inf, C_AGAIN);
			return;

			/* Waiting for Restore */
		case WRESTORE:
			inf->inf_ccyl = 0;
			dskerror(inf, stat, DEV000);
			return;

		default:
			printf("dsk %x not waiting for %x\n",
				addr, inf->inf_active);
			return;
		}
}

/*
 *   Selch interrupt routine.
 *	- Selch interrupt will be followed by a controller interrupt.
 *	  The controller interrupt better occur, since I/O termination
 *	  is determined by that interrupt, not this one.
 */
dsksintr(dev, stat, inf)
register struct inf *inf;
{
	register struct buf *bp;
	register selch;

	if (!inf)  {
		printf("dsk selch not busy\n");
		return;
		}

	selch = inf->inf_selch;
	trace(0x8000, "selch intr", selch);
	oc(selch, STOP|RDEXAD);
	stat = ss(inf->inf_cntl);
	if (stat & OVERRUN)  {
		/* Controller will not interrupt */
		timer(inf, 0);
		oc(selch, STOP|SELSTAT);
		trace(0x4000, "dskserr", ss(selch));
		inf->inf_flags |= OVERRUN;
		dskerror(inf, stat, CONTROLLER);
		}

	return;
}


/*
 *   Disc controller interrupt routine.
 *	- Check ending status.
 *	- Complete I/O operation.
 */
dskcintr(dev, stat)
{
	register struct buf *bp;
	register struct inf *inf;
	register selch;
	register st;

	inf = cntltab[dev].c_inf;
	cntltab[dev].c_inf = 0;
	if (!inf  ||  inf->inf_active != WIO)  {
		printf("dskcntl interrupt %x\n", dev);
		return;
		}

	trace(0x8000, "interrupt", inf->inf_cntl);
	trace(0x8000, "status", stat);

	if (!(bp = inf->inf_actf))  {
		printf("dskcintr: no work for %x\n", inf->inf_cntl);
		return;
		}

	if (stat & CNTL_UNRCV)  {
		dskerror(inf, stat, CONTROLLER);
		return;
		}

	timer(inf, 0);
	bp->b_resid = 0;
	inf->inf_active = WRELEASE;
	selch = inf->inf_selch;
	oc(selch, STOP|SELSTAT);
	st = ss(selch);
	oc(selch, STOP|RDEXAD);
	inf->inf_fad = rdh(selch);

	if (st & (SELCHMM|SELCHPF))  {
		/* Transfer failed (memory error) */
		dskerror(inf, st, CHANNEL);
		return;
		}

	if (stat & CYL_OV)  {
		/*
		 *   Cylinder overflow
		 *	- Initiate new seek to complete the I/O
		 */
		inf->inf_sad += (inf->inf_fad - inf->inf_sad + 2) & ~0xff;
		trace(0x4000, "cyl ov", inf->inf_sad);
		inf->inf_sector = 0;
		inf->inf_head   = 0;
		inf->inf_fad    = 0;
		inf->inf_flags |= CYL_OV;
		inf->inf_errcnt = 0;
		bp->b_resid = inf->inf_ead - inf->inf_sad;
		if (inf->inf_flags & DUAL)  {
			/* Dual platter - simulate head change */
			if (inf->inf_flags & LOWER)
				/* Switch cylinders for real */
				inf->inf_cyl++;

			inf->inf_flags ^= LOWER;
			}
		else  {
			/* Single platter - switch cylinders */
			inf->inf_cyl++;
			}

		if (inf->inf_cyl < NCYL(inf))  {
			/* New cyl number still in range - just restart */
			inf->inf_active = WSTART;
			dskstart(inf);
			return;
			}
		else
			if ((bp->b_flags & (B_PHYS|B_READ)) != (B_PHYS|B_READ))
				bp->b_flags |= B_ERROR;
		}


	dskiodone(inf, 0);
	dskrelease(inf);
	dsksync(inf, C_IDLE);
	return;
}


/*
 *   Release.
 *	- Simply reset states in the inf structure.
 */
dskrelease(inf)
register struct inf *inf;
{
	register int nc;
	register stat;

	nc   = inf->inf_ncntl;
	trace(0x4000, "release", ADDR(inf));
	if (inf->inf_dn >= 0)
		dk_busy &= ~(1<<inf->inf_dn);

	inf->inf_active = DNACT;

	Selchfree(inf);
	cntltab[nc].c_inf   = 0;
	cntltab[nc].c_state = C_IDLE;
	return;
}


/*
 *   Clean up after I/O.
 *	- De-Queue request for I/O just completed.
 *	- Queue next request.
 */
dskiodone(inf, flag)
register struct inf *inf;
register flag;
{
	register struct buf *bp;

	inf->inf_flags = 0;
	inf->inf_errcnt = 0;
	bp = inf->inf_actf;
	inf->inf_actf = bp->av_forw;
	bp->b_flags |= flag;
	iodone(bp);
	return;
}


/*
 *   Time out handler.
 */
dsktimeout(inf)
register struct inf *inf;
{
	switch (inf->inf_active)  {
		case WSEEK:
			dskerror(inf, 0, DEV000);
			break;

		case WIO:
			dskio(inf);
			break;
		}

	printf("dsk %x time out on %d\n", inf, inf->inf_active);
	return;
}


/*
 *   Called to re-try I/O if possible.
 *	- Controller must be idle when called.
 *	- Can only be called after a seek has completed.
 *	  Different strategy must be used for pre-seek complete errors.
 */
dskerror(inf, stat, device)
register struct inf *inf;
{
	register struct buf *bp;
	register addr;
	register int nc;

	CNTLIDLE(inf->inf_cntl);
	bp = inf->inf_actf;
	nc = inf->inf_ncntl;
	switch (device)  {

			/* Device - clear error */
		case DEVICE:
			addr = ADDR(inf);
			deverror(bp, stat, addr);
			inf->inf_active = WRESTORE;
			oc(addr, RESTORE|ENABLE);
			return;

			/* Controller - should reset? */
		case CONTROLLER:
			deverror(bp, stat, inf->inf_cntl);
			break;

			/* Channel - serious error */
		case CHANNEL:
			deverror(bp, stat, inf->inf_selch);
			break;

			/* Restore complete - continue recovery */
		case DEV000:
			break;
		}

	if (inf->inf_dn >= 0)
		dk_busy &= ~(1 << inf->inf_dn);

	inf->inf_ccyl = OFFCYL;		/* force seek */
	if (++inf->inf_errcnt <= NRETRY)  {
		/* Initiate re-try as normal I/O */
		inf->inf_active = WSTART;
		Selchfree(inf);
		cntltab[nc].c_inf   = 0;
		cntltab[nc].c_state = C_IDLE;
		dsksync(inf, C_IDLE);
		return;
		}

	/*
	 *   Solid error.
	 */
	printf("Solid error\n");
	dskiodone(inf, B_ERROR);
	dskrelease(inf);
	dsksync(inf, C_IDLE);
	return;
}


/*
 *   Re-synchronization routine.
 *	- Called after disc not ready by clock routine.
 *	- Use common sync routine to restart.
 */
dsknrdy(inf)
register struct inf *inf;
{
	inf->inf_active = DNACT;
	dsksync(inf, C_AGAIN);
	return;
}


/*
 *   'Raw' disc interface.
 *	- Should check minor dev # is in range, but this is checked
 *	  by the strategy routine when called later on.
 */
dskread(dev)
{
	register int n;

	n = dskinf[DRIVENUMBER(dev)].inf_ncntl;
	physio(dskstrategy, &rawbuf[n], dev, B_READ);
}

dskwrite(dev)
{
	register int n;

	n = dskinf[DRIVENUMBER(dev)].inf_ncntl;
	physio(dskstrategy, &rawbuf[n], dev, B_WRITE);
}


dskdump()
{
	register struct inf *inf;
	register cyl, len;
	register char *memp;
	int dcyl;

	/* Make sure this is the right driver */
	if (bdevsw[major(swapdev)].d_strategy != dskstrategy)  {
		printf("swap device is not on a dsk\n");
		return;
		}

	/* Set up device addresses */
	inf = &dskinf[DRIVENUMBER(swapdev)];
	if (DUALDISC(swapdev))  {
		printf("Dump on dual dsk not allowed\n");
		return;
		}

	if (PLATTER(swapdev) == FIXED)
		inf->inf_flags |= LOWER;

	/* Dump from memory address 0 */
	memp = (char *)0;
	dcyl = cyl = (swplo + 1 + nswap - ((memtop+BSIZE)>>BSHIFT)) / NBPC(inf);
	while ((len = memtop-memp) > 0) {
		/* Dump a cylinder at a time */
		if (len > NBPC(inf)*BSIZE)
			len = NBPC(inf)*BSIZE;
		oc(inf->inf_selch, STOP);
		wh(ADDR(inf), cyl);
		oc(ADDR(inf), SEEK);
		while (ss(ADDR(inf)) != 0)
			;
		wdh(inf->inf_selch, memp);
		wdh(inf->inf_selch, memp + len - 1);
		wh(ADDR(inf), cyl);
		wd(inf->inf_cntl, 0);
		oc(inf->inf_cntl, WRITE);
		oc(inf->inf_selch, GO);
		while (ss(inf->inf_selch)&SELCHBSY)
			;
		oc(inf->inf_selch, STOP);
		while (ss(inf->inf_cntl) != IDLE)
			;
		memp += len;
		cyl++;
		}

	printf("Dumped to cyl %d\n", dcyl);
}
