/*
 *   MSM disc driver
 *	(C) Ross Nealon, 1982
 *	Suitable for MSM 80, 300
 */

#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		ALTBSY		0x20
#define		UNSAFE		0x10
#define		UNREADY		0x08
#define		SEEKINC		0x02
#define		OFFLINE		0x01
#define		DSK_UNRCV	(ALTBSY | UNSAFE | UNREADY | SEEKINC | OFFLINE)

#define		SETHEAD		0x20
#define		SETCYL		0x10
#define		SEEK		0x02
#define		RELEASE		0xb0
#define		RESTORE		0x01
#define		SERVO		0x30
#define		PLUS		0x08
#define		MINUS		0x04
#define		EARLY		0x02
#define		LATE		0x01
#define		CLRFAULT	0x70

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

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


/*
 *   Macros
 */
#define		CNTLIDLE(c)	while ((ss(c) & IDLE) == 0)
#define		DRIVENUMBER(d)	(minor(d) >> 3)
#define		SUBDISC(d)	(minor(d) & 07)

#define		NCYL(i)		(823)
#define		NBPC(i)		(msmtnbpc[i->inf_type])
#define		NBPT(i)		(32)

/*
 *   Tables
 */
extern		int	nmsm;			/* number of discs */
extern	struct	buf	msmtab;			/* devtab, in bdevsw.d_tab */
extern	struct	inf	msminf[ ];		/* info table */
extern		int	msmstart();		/* I/O startup routine */
extern		int	msmsintr();		/* channel interrupt routine */
extern		int	msmtimeout();		/* time out handler */

static	int	msmtnbpc[ ]	=	{	/* blocks / cyl / dev type */
		  0,				/* illegal type */
		160,				/* MSM80 */
		608,				/* MSM300 */
		  0,				/* CDD16+16 */
		  0,				/* CDD48+16 */
		  0,				/* CDD80+16 */
	};

static	int	msmservo[ ]	=	{	/* seek servo commands */
		SERVO,				/*   try 0 */
		SERVO,				/* retry 1 */
		SERVO | EARLY,			/* retry 2 */
		SERVO | LATE,			/* ...     */
		SERVO | LATE | MINUS,
		SERVO | LATE | PLUS,
		SERVO | MINUS,
		SERVO | PLUS,
		SERVO | EARLY | MINUS,
		SERVO | EARLY | PLUS,	
		SERVO,
	};



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

	bp->b_resid = bp->b_bcount;
	if (DRIVENUMBER(bp->b_dev) >= nmsm)  {
		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 = &msminf[DRIVENUMBER(bp->b_dev)];
	if (bp->b_blkno >= inf->inf_map[SUBDISC(bp->b_dev)].dm_size)  {
		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);

	msmsync(inf, C_IDLE);
	return;
}


/*
 *   Synchronization routine.
 *	- Ensure only one `driver' is operative at a time.
 */
msmsync(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(0x80<<16, "msmbusy", state);
		return;
		}

	/* Controller idle - start up I/O driver */
	Selchreq(inf, &msmstart, &msmsintr);
	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.
 */
msmstart(infst)
struct inf *infst;
{
	register struct inf *inf;
	register struct inf *infio;
	register int nc;
	register int i;
	register state;
	register s;

	trace(0x40<<16, "msmstart", 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<nmsm;  i++)  {
		inf++;
		if (inf >= &msminf[nmsm])
			inf = &msminf[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 (!msmsetup(inf))  {
				/* I/O would have failed */
				msmiodone(inf, B_ERROR);
				continue;
				}

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

		if (inf->inf_active == WSTART)  {
			/* Start up a seek */
			msmseek(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;
		msmio(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 msmstart */
		msmsync(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.
 */
msmsetup(inf)
register struct inf *inf;
{
	register struct buf *bp;
	register long bn;
	register int cyl;
	register int sz;

	bp = inf->inf_actf;
	bn = bp->b_blkno + inf->inf_map[SUBDISC(bp->b_dev)].dm_offset;
	sz = bp->b_bcount / BSIZE;
	if (bp->b_bcount % BSIZE)
		sz++;

	cyl = bn / NBPC(inf);
	bn %= NBPC(inf);
	if (cyl > NCYL(inf))
		return(0);


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



	inf->inf_cyl	= cyl;
	inf->inf_head	= (bn / NBPT(inf));
	inf->inf_sector	= (bn % NBPT(inf)) << 1;
	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.
 */
msmseek(inf)
register struct inf *inf;
{
	register int cyl;
	register addr, cntl;
	register stat;

	addr = inf->inf_addr;
	cntl = inf->inf_cntl;
	cyl  = inf->inf_cyl;
	trace(0x80<<16, "seek", addr);
	trace(0x80<<16, "cyl", cyl);

	CNTLIDLE(cntl);
	stat = ss(addr);
	if (stat == (UNREADY|OFFLINE))  {
		/* wait until it comes back */
		trace(0x80<<16, "offline", stat);
		inf->inf_active = DNRDY;
		oc(addr, ENABLE);
		return;
		}

	if (stat & ALTBSY)  {
		trace(0x80<<16, "alt", stat);
		inf->inf_active = WALT;
		timer(inf, 10, &msmtimeout, inf);
		oc(addr, ENABLE);
		return;
		}

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

	trace(0x80<<16, "stat", stat);
	inf->inf_active = WSEEK;
	timer(inf, 3, &msmtimeout, inf);
	inf->inf_last = inf->inf_ccyl;
	inf->inf_ccyl = SEEKING;

	wh(addr, cyl);
	CNTLIDLE(cntl);
	oc(addr, SETCYL|DISARM);
	CNTLIDLE(cntl);
	oc(addr, msmservo[0]);
	oc(addr, SEEK|ENABLE);
	CNTLIDLE(cntl);
}


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

	trace(0x40<<16, "msmio", inf);
	addr  = inf->inf_addr;
	cntl  = inf->inf_cntl;
	selch = inf->inf_selch;
	oc(selch, STOP);
	stat  = ss(addr);
	if (stat & DSK_UNRCV)  {
		msmerror(inf, stat, DEVICE);
		return;
		}

	trace(0x40<<16, "sad", inf->inf_sad);
	trace(0x40<<16, "ead", inf->inf_ead);

	wdh(selch, inf->inf_sad);
	wdh(selch, inf->inf_ead);
	wh(addr, inf->inf_head);
	oc(addr, SETHEAD|DISARM);
	CNTLIDLE(cntl);

	wd(cntl, inf->inf_sector);
	wh(cntl, (inf->inf_head<<10)|inf->inf_cyl);
	wh(addr, inf->inf_head);
	oc(addr, SETHEAD|DISARM);
	CNTLIDLE(cntl);

	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, &msmtimeout, inf);
	splx(s);
	return;
}


/*
 *   Disc interrupt routine - interrupt occurs after
 *	- A seek completes.
 *	- Alternate Channel Busy clears.
 *	- Restore command completes.
 *	- Drive goes into a funny state.
 */
msmdintr(dev, stat)
{
	register struct inf *inf;
	register addr;

	inf  = &msminf[dev];
	addr = inf->inf_addr;
	trace(0x80<<16, "interrupt", addr);
	trace(0x80<<16, "status", stat);

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

		inf->inf_active = DNACT;
		return;
		}

	switch (inf->inf_active)  {

			/* Disc back on line */
		case DNRDY:
			inf->inf_active = WSTART;
			msmsync(inf, C_AGAIN);
			return;

			/* Alternate channel free - repeat I/O attempt */
		case WALT:
			timer(inf, 0);
			inf->inf_active = WSTART;
			msmsync(inf, C_AGAIN);
			return;

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

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

		default:
			printf("msm %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.
 */
msmsintr(dev, stat, inf)
register struct inf *inf;
{
	register struct buf *bp;
	register selch;

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

	selch = inf->inf_selch;
	trace(0x80<<16, "selch intr", selch);
	oc(selch, STOP|RDEXAD);
	if (inf->inf_sad == rdh(selch))  {
		/* Selch did not complete transfer */
		oc(selch, STOP|SELSTAT);
		trace(0x40<<16, "msmserr", ss(selch));
		/* Wait for controller interrupt to handle error */
		}

	return;
}


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

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

	trace(0x80<<16, "interrupt", inf->inf_cntl);
	trace(0x80<<16, "status", stat);

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

	if (stat & CNTL_UNRCV)  {
		msmerror(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) */
		msmerror(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(0x40<<16, "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;
		dm = &inf->inf_map[SUBDISC(bp->b_dev)];
		inf->inf_cyl++;
		if ((inf->inf_cyl*NBPC(inf)) < dm->dm_offset+dm->dm_size)  {
			/* New cyl number still in range - just restart */
			inf->inf_active = WSTART;
			msmstart(inf);
			return;
			}
		else
			if ((bp->b_flags & (B_PHYS|B_READ)) != (B_PHYS|B_READ))
				bp->b_flags |= B_ERROR;
		}


	msmiodone(inf, 0);
	msmrelease(inf);
	msmsync(inf, C_IDLE);
	return;
}


/*
 *   Release the drive for a possible Alternate Channel access.
 */
msmrelease(inf)
register struct inf *inf;
{
	register int nc;
	register addr, cntl;
	register stat;

	addr = inf->inf_addr;
	cntl = inf->inf_cntl;
	nc   = inf->inf_ncntl;
	trace(0x40<<16, "release", addr);
	if (inf->inf_dn >= 0)
		dk_busy &= ~(1<<inf->inf_dn);

	oc(cntl, DISARM);
	CNTLIDLE(cntl);
	oc(addr, RELEASE);
	stat = ss(addr);
	if (stat != (UNREADY|OFFLINE))
		printf("msm %x not released\n", addr);

	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.
 */
msmiodone(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.
 */
msmtimeout(inf)
register struct inf *inf;
{
	register stat;

	switch (inf->inf_active)  {
		case WALT:
			msmerror(inf, 0, DEV000);
			break;

		case WSEEK:
			stat = ss(inf->inf_addr);
			if (stat & SEEKINC)
				msmerror(inf, stat, DEVICE);
			else
				msmerror(inf, 0, DEV000);
			break;

		case WIO:
			msmio(inf);
			break;
		}

	printf("msm %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.
 */
msmerror(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 = inf->inf_addr;
			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;
		msmsync(inf, C_IDLE);
		return;
		}

	/*
	 *   Solid error.
	 */
	printf("Solid error\n");
	msmiodone(inf, B_ERROR);
	msmrelease(inf);
	msmsync(inf, C_IDLE);
	return;
}


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

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

msmwrite(dev)
{
	register int n;

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


/*
 *   Stand-alone post-mortem dump:
 *	Dump all of memory to the end of the swap space.
 *	Runs via 'dump'
 */
msmdump()
{
	register struct inf *inf;
	register cyl, len;
	register char *memp;
	register long swp;
	int dcyl;

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

	/* Set up device addresses */
	inf = &msminf[DRIVENUMBER(swapdev)];

	/* Dump from memory address 0 */
	memp = (char *)0;
	swp = swplo + inf->inf_map[SUBDISC(swapdev)].dm_offset;
	dcyl = cyl = (swp + 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);
		while (ss(inf->inf_addr) & ALTBSY)
			;
		wh(inf->inf_addr, cyl);
		CNTLIDLE(inf->inf_cntl);
		oc(inf->inf_addr, SETCYL|DISARM);
		CNTLIDLE(inf->inf_cntl);
		oc(inf->inf_addr, msmservo[0]);
		oc(inf->inf_addr, SEEK);
		while (ss(inf->inf_addr) != 0)
			;
		wdh(inf->inf_selch, memp);
		wdh(inf->inf_selch, memp + len - 1);
		wh(inf->inf_addr, 0);
		oc(inf->inf_addr, SETHEAD);
		CNTLIDLE(inf->inf_cntl);
		wh(inf->inf_cntl, 0);		/* sector */
		wh(inf->inf_cntl, cyl);
		wh(inf->inf_addr, 0);		/* head */
		oc(inf->inf_addr, SETHEAD);
		CNTLIDLE(inf->inf_cntl);
		oc(inf->inf_cntl, WRITE);
		oc(inf->inf_selch, GO);
		while (ss(inf->inf_selch)&SELCHBSY)
			;
		oc(inf->inf_selch, STOP);
		CNTLIDLE(inf->inf_cntl);
		memp += len;
		cyl++;
		}

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