/* 
 * Driver for cipher tape drive support for Linux-i386
 * Based on DataTrac/PCT General GPTR 3.2
 * Linux version 0.0
 * 
 * The portions of this software based on that code are provided
 * exclusively for use with DataTrac products.  Use with any other
 * products constitutes theft.
 *
 * Copyright (c) 1995 by Thomas J. Moore. All rights reserved.
 * Current e-mail address: dark@mama.indstate.edu
 *
 * Distribution of this program in executable form is only allowed if
 * all of the corresponding source files are made available through the same
 * medium at no extra cost.
 *
 * I will not accept any responsibility for damage caused directly or
 * indirectly by this program, or code derived from this program.
 *
 * Use this code at your own risk. Don't blame me if it destroys your data!
 * Make sure you have a backup before you try this code.
 *
 * If you make changes to my code and redistribute it in source or binary
 * form you must make it clear to even casual users of your code that you
 * have modified my code, clearly point out what the changes exactly are
 * (preferably in the form of a context diff file), how to undo your changes,
 * where the original can be obtained, and that complaints/requests about the
 * modified code should be directed to you instead of me.
 *
 */

/*
 * This is a driver for the cipher 9000 series card, based
 * on the GPTR assembler routines provided with the card.
 * Some stuff stolen from the scsi and qic tape drivers.
 * Don't even THINK about trying the ATC-8 stuff yet...
 */

#include <linux/autoconf.h>

#ifdef MODULE
#include <linux/module.h>
#include <linux/version.h>
#else
#define MOD_INC_USE_COUNT
#define MOD_DEC_USE_COUNT
#endif

#define REALLY_SLOW_IO

#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/errno.h>
#include <linux/mtio.h>
#include <linux/fcntl.h>
#include <linux/delay.h>
#include <linux/config.h>
#include <linux/cipher.h>
#include <linux/mm.h>
#include <linux/ioport.h>
#include <linux/malloc.h>

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

/* check existence of required configuration parameters */
#if !defined(CIPHER_TAPE_PORT) || \
    !defined(CIPHER_TAPE_IRQ) || \
    !defined(CIPHER_TAPE_DMA) || \
    !defined(CIPHER_TAPE_ADDR)
#error cipher_tape configuration error
#endif

#define CIPHER_NAME	"cipher"

#define TCB struct tape_ctrl_block

static TCB _tcb[0]; /* for now just one unit is supported */

static int purge_tbuffer(TCB *);
static int cmdwt(TCB *, unsigned char);
static int read_buffer(TCB *tcb);
static int write_buffer(TCB *tcb);
static int purge_read(TCB *);
static int purge_write(TCB *);
static int cipher_error(TCB *);

static void ifc_init(TCB *tcb)
{
    memset(tcb, 0, sizeof(*tcb));
    tcb->drive_select = (CIPHER_TAPE_ADDR<<4)|DT_CCG|DT_ENABLE;
}

#if CIPHER_TAPE_IFC == AT_16
#define CMD(x) outw_p((tcb->drive_select|(x))<<8, DT_COMMAND)
#define STROBE(x) outw_p(0, x)
#define STATUS inw_p(DT_STATUS)
#define STATUS2 inw_p(DT_STATUS)
#else
#define CMD(x) outb_p(tcb->drive_select|(x), DT_COMMAND)
#define STROBE(x) outb_p(0, x)
#define STATUS inb_p(DT_STATUS)
#define STATUS2 ((inb_p(DT_FLAGS) << 8) | inb_p(DT_STATUS))
#endif

/* select drive */
static inline int select(TCB *tcb)
{
    /* enable the formatter */
    CMD(0);
#if CIPHER_TAPE_IFC == AT_16
    /* enable the interface card */
    outw_p(0x9200, DT_CONFIG); /* interface enable (DBUSY INTERRUPT) */
    /* check to see if drive is ready */
    if(!(STATUS & DT_READY)) {
	outw_p(0, DT_CONFIG);
	return tcb->error = TERR_NOTREADY;
    }
#else /* ATC_8 */
    /* check to see if drive is ready */
    if(!(STATUS & DT_READY))
	/* return DRIVE NOT READY */
	return tcb->error = TERR_NOTREADY;
#endif
    return tcb->error = TERR_OK;
}

/* rewind tape */
static inline void jrew(TCB *tcb)
{
    if(purge_tbuffer(tcb))
    	return;
    if(select(tcb))
    	return;
    CMD(DT_REW);
    STROBE(DT_CLEARF);
    /* wait until either online is dropped or BOT and READY are set */
    do {
#if CIPHER_TAPE_IFC == AT_16
	if(!(inw_p(DT_EXTENDED) & DT_EX_ONLINE)) {
	    outw_p(0, DT_CONFIG);
	    tcb->error = TERR_NOTREADY;
	    return;
	}
#else
	if(!(inb_p(DT_FLAGS) & DT_ONLINE))
	    return tcb->error = TERR_NOTREADY;
#endif
    } while((STATUS & (DT_LOADPT|DT_READY)) != (DT_LOADPT|DT_READY));
#if CIPHER_TAPE_IFC == AT_16
    outw_p(0, DT_CONFIG);
#endif
    tcb->recno = tcb->fileno = 0;
}

/* purge buffers */
static int purge_tbuffer(TCB *tcb)
{
    if(!tcb->rwmode)
    	return purge_read(tcb);
    else
    	return purge_write(tcb);
}

/* out of gptr.fun */
/* issue command and wait */
static int cmdwt(TCB *tcb, unsigned char cmd)
{
    int i;
    unsigned short cmds;

    save_flags(i);
    cli();
#if CIPHER_TAPE_IFC == AT_16
    STROBE(DT_CLEARF);
    STROBE(DT_CLEAR_DBUSY);
#if CIPHER_TAPE_DMA > 4
    outw_p(0x9200|DT_INTMASK|tcb->bus_temp, DT_CONFIG);
#else
    outw_p(0x9300|DT_INTMASK|tcb->bus_temp, DT_CONFIG);
#endif
    tcb->bus_temp = 0;
    /* send command & toggle GO strobe */
    cmds = tcb->drive_select|tcb->temp_sel;
    cmds = (cmds<<8)|cmd;
    outw_p(cmds, DT_COMMAND);
    outw_p(cmds|DT_GO, DT_COMMAND);
    outw_p(cmds, DT_COMMAND);
    /* make sure command is taken */
    if(!(inw_p(DT_EXTENDED) & (DT_EX_FBUSY | DT_INT_DBUSY))) {
	restore_flags(i);
	outw_p(0, DT_CONFIG);
	tcb->temp_sel = 0;
	return tcb->error = TERR_CMDLOST;
    }
    /* wait for completion of command */
    for(;;) {
	if(inw_p(DT_EXTENDED) & DT_INT_DBUSY) { /* if not done, wait */
	    if(inw_p(DT_EXTENDED) & DT_EX_DBUSY) { /* if data busy, then noise interrupt */
		STROBE(DT_CLEAR_DBUSY);
		if(!(inw_p(DT_EXTENDED) & DT_EX_DBUSY)) /* unless slow... */
	    	    break; /* done */
	    } else
	    	break;
	}
	/* have busy interface, so wait */
	if(!(inw_p(DT_EXTENDED) & DT_EX_FBUSY)) { /* if formatter still busy... */
	    if(!(inw_p(DT_EXTENDED) & DT_INT_DBUSY)) {
		restore_flags(i);
		outw_p(0, DT_CONFIG);
		tcb->temp_sel = 0;
		return tcb->error = TERR_CMDLOST;
	    } else
	    	break; /* OK if completed */
	}
	/* dbusy is low, so interrupt is bound to occur at end */
	interruptible_sleep_on(&tcb->wait_queue);
	cli();
    }
    /* command has completed; clean up & exit */
    restore_flags(i);
    if(tcb->bus_temp)
    	outw_p(0, DT_CONFIG);
    tcb->temp_sel = 0;
    STROBE(DT_CLEAR_DBUSY);
    return tcb->error = TERR_OK;
#else /* ATC-8 */
    CMD(tcb->temp_sel);
    if(!(STATUS & DT_READY))
    	return 3;
    STROBE(DT_FLAGS_ONLY);
    STROBE(DT_CLEARI);
    STROBE(DT_CLEAR_TC);
#if CIPHER_TAPE_IRQ > 0
    outb_p(tcb->bus_temp|DT_INTMASK|DT_INT, DT_CONFIG);
    INITIALIZE_INTERRUPT
    ENABLE_INTERRUPT
#endif
    outb_p(cmd, DT_COMMAND);
    PUSHF
    cli();
    outb_p(cmd | DT_GO, DT_COMMAND);
    nop();
    nop();
    outb_p(cmd, DT_COMAND);
    POPF
    /* make sure command is taken */
    if(!(inb_p(DT_EXTENDED) & DT_FBUSY_STATUS) || !(inw_p(DT_EXTENDED) & DT_DBUSY_INT)) {
	DISABLE_INTERRUPT
	STROBE(DT_FUNCTION);
	tcb->temp_sel = 0;
	tcb->bus_temp = 0;
	return 11;
    }
    /* wait for completion of command */
    for(;;) {
	i = 0;
	if(inb_p(DT_EXTENDED) & DT_DBUSY_INT) { /* if not done, wait */
	    if(inb_p(DT_EXTENDED) & DT_DBUSY_STATUS) { /* if data not busy */
		STROBE(DT_CLEARI); /* clear noise interrupt */
		if(!(inb_p(DT_EXTENDED) & DT_DBUSY_STATUS))
	    	    break;
	    } else
	    	i = 1;
	}
	if(!i) { /* have busy interface, so wait */
	    PUSHF
	    cli();
	    if(!(inb_p(DT_EXTENDED) & DT_FBUSY_STATUS)) {
	    	if(!(inb_p(DT_EXTENDED) & DT_DBUSY_INT)) {
		    POPF
		    break; /* OK if completed */
		} else {  /* error if not */
		    DISABLE_INTERRUPT
		    STROBE(DT_FUNCTION);
		    tcb->temp_sel = 0;
		    tcb->bus_temp = 0;
		    return 11;
		}
	    } else { /* dbusy is low, so interrupt will occur at end */
		POPF
#if CIPHER_TAPE_IRQ > 0
		nop();
		hlt();
#endif
		/* check to see if done */
		if(!(inb_p(DT_EXTENDED) & DT_DBUSY_INT))
		    continue; /* if not done, wait */
		if(!(inb_p(DT_EXTENDED) & DT_DBUSY_STATUS))
		    break;
		/* noise interrupt; completion flag set yet still busy */
		STROBE(DT_CLEARI); /* clear noise interrupt */
		if(inb_p(DT_EXTENDED) & DT_DBUSY_STATUS)
		    continue;
	    }
	}
    }
    /* command has completed; clean up & exit */
    DISABLE_INTERRUPT
    STROBE(DT_CLEARI);
    return tcb->error = TERR_OK;
#endif
}

/* from gptr.rdr */
static int jread(TCB *tcb, void *dest, int len)
{
    tcb->error = TERR_OK;
    if(tcb->rwmode)
    	purge_tbuffer(tcb);
    if(!tcb->after) {
	if(read_buffer(tcb)) {
	    if(tcb->error != TERR_EOF) {
	    	printk("cipher: read error: %d\n", tcb->error);
		tcb->recno = -1;
	    } else
	    	tcb->recno = 0;
	    return -tcb->error;
	} else
	    tcb->recno++;
    }
    /* have data in buffer, now transfer as required */
    if(len > tcb->after)
    	len = tcb->after;
    tcb->after -= len;
    memcpy_tofs(dest, tcb->buffer+tcb->before, len);
    tcb->before += len;
    tcb->resid = tcb->after;
    return len;
}

/* actual read device handler */
/* read physical tape block into buffer */
#if CIPHER_TAPE_IFC == AT_16
int read_buffer(TCB *tcb)
{
    int i, j;
#if CIPHER_TAPE_DMA <= 4
    unsigned short *bp;
#endif

    tcb->before = tcb->after = 0;
    if(select(tcb))
    	return tcb->error;
    tcb->temp_sel = DT_GATE|DT_CCG;
    CMD(tcb->temp_sel);
#if CIPHER_TAPE_DMA > 4
    disable_dma(CIPHER_TAPE_DMA);
    clear_dma_ff(CIPHER_TAPE_DMA);
#endif
    tcb->bus_temp = DT_DMAMASK;
    outw_p(0x9200|tcb->bus_temp, DT_CONFIG);
    STROBE(DT_CLEAR_FIFO);
#if CIPHER_TAPE_DMA > 4
    STROBE(DT_CLEAR_TC);
    set_dma_addr(CIPHER_TAPE_DMA, (unsigned long)tcb->buffer);
    /* set up DMA controller */
    set_dma_mode(CIPHER_TAPE_DMA, DMA_MODE_READ);
/*    set_dma_count(CIPHER_TAPE_DMA, (tcb->bufsiz+1)/2); */
    set_dma_count(CIPHER_TAPE_DMA, (tcb->bufsiz+1));
    enable_dma(CIPHER_TAPE_DMA);
#endif
    cmdwt(tcb,DT_READ);	/* read command with strobe */

    /* have read data, now get length of trans
     * by reading updated address pointer
     * from dma ctrl */
    tcb->last = inw_p(DT_EXTENDED);
    if(tcb->last & DT_EX_ODD) {
	STROBE(DT_CYCLE);
	/* should force bus cycles here */
	nop();
	nop();
	nop();
	nop();
    }
#if CIPHER_TAPE_DMA > 4
    do {
	i = inw_p(DT_EXTENDED);
    } while((i & DT_EX_NOTEMPTY) && !(i & DT_INT_TC));
    disable_dma(CIPHER_TAPE_DMA);
#endif
    /* now check to se if read good */
    if(STATUS & DT_FMK) {
	tcb->last = 0;
	outw_p(0, DT_CONFIG);
	return tcb->error = TERR_EOF;
    }
#if CIPHER_TAPE_DMA > 4
    /* dma fetch length */
    if(inw_p(DT_EXTENDED) & DT_EX_DMAREQ) {
	tcb->last = 0;
	return tcb->error = TERR_RDOVF;
    }
    outw_p(0, DT_CONFIG);
    i = tcb->bufsiz/2 - get_dma_residue(CIPHER_TAPE_DMA)/2;
    if(!i) {
    	tcb->last = 0;
	return tcb->error = TERR_NCORRERR;
    }
#else
    /* fetch length & data */
    if(!(inw_p(DT_EXTENDED) & DT_EX_DMAREQ)) {
	outw_p(0, DT_CONFIG);
	return tcb->error = TERR_NCORRERR;
    }
    i = inw_p(DT_COUNTER);
    if(i > tcb->bufsiz/2) {
	outw_p(0, DT_CONFIG);
    	return tcb->error = TERR_RDOVF;
    }
    if(tcb->last & DT_EX_ODD)
	i++;
    bp = tcb->buffer;
    while(i--)
    	*bp++ = inw_p(DT_RDATA);
    outw_p(0, DT_CONFIG);
    i = inw_p(DT_COUNTER);
#endif
    /* data in place, length in words (less one if odd) is i */
    j = STATUS;
    if(!(j & DT_HER)) {
	if(j & DT_OVERFLOW) {
	    tcb->last = 0;
	    return tcb->error = TERR_DMAOVF;
	}
    } else {
	if(i > 9) {
	    tcb->last = 0;
	    return tcb->error = TERR_NCORRERR;
	}
	tcb->error = TERR_TRIVBLK;
	read_buffer(tcb); /* noise block; attempt reread */
	return tcb->error;
    }
    if(j & DT_CER)
    	tcb->error = TERR_CORRERR;
    inw_p(DT_EXTENDED);
    i *= 2;
    if(tcb->last & DT_EX_ODD)
    	i--;
    tcb->after = tcb->last = i;
    return tcb->error;
}
#endif

static int purge_read(TCB *tcb)
{
    tcb->before = tcb->after = 0;
    return 0;
}

/* from gptr.wrt */
int jwrite(TCB *tcb, void *dest, int len)
{
    tcb->error = TERR_OK;
    if(!len)
    	return tcb->error;
    if(!tcb->rwmode) {
	tcb->after = tcb->blksiz;
	tcb->before = 0;
    }
    tcb->rwmode = 1;
    if(len > tcb->after)
    	len = tcb->after;
    memcpy_fromfs(tcb->buffer+tcb->before, dest, len);
    tcb->before += len;
    tcb->after -= len;
    if(!tcb->after) {
	if(!write_buffer(tcb))
	    tcb->recno++;
	else
	    tcb->recno = -1;
    }
    if(tcb->error) {
	printk("cipher: write error: %d\n", tcb->error);
    	return -tcb->error;
    }
    tcb->resid = tcb->after;
    return len;
}

void write_reset_counters(TCB *tcb)
{
    tcb->rwmode = 0;
    tcb->before = tcb->after =0;
}

#if CIPHER_TAPE_IFC == AT_16
int write_buffer(TCB *tcb)
{
#if CIPHER_TAPE_DMA <= 4
    int i, j, s;
    unsigned short *bp;
#else
    int j, s;
#endif

    for(j=0; j< 10; j++) {
#if CIPHER_TAPE_DMA > 4
	disable_dma(CIPHER_TAPE_DMA);
	clear_dma_ff(CIPHER_TAPE_DMA);
#endif
	if(select(tcb))
    	    return tcb->error;
	tcb->bus_temp = DT_DMAMASK;
	if(tcb->before & 1)
    	    tcb->bus_temp |= 0x80;
	outw_p(((CIPHER_TAPE_DMA > 4) ? 0x9200 : 0x9300) |tcb->bus_temp, DT_CONFIG);
	STROBE(DT_CLEAR_FIFO);
	STROBE(DT_CLEAR_TC);
#if CIPHER_TAPE_DMA > 4
	/* set up DMA controller */
	set_dma_mode(CIPHER_TAPE_DMA, DMA_MODE_WRITE);
	set_dma_addr(CIPHER_TAPE_DMA, (unsigned long)tcb->buffer);
	set_dma_count(CIPHER_TAPE_DMA, (tcb->before+1));
/*	set_dma_count(CIPHER_TAPE_DMA, (tcb->before+1)/2); */
	enable_dma(CIPHER_TAPE_DMA);
	nop();
	nop();
	nop();
	nop();
#else
	/* no dma; pump data into fifo */
	STROBE(DT_CLEAR_FIFO);
	for(i=(tcb->before+1)/2, bp = tcb->buffer; i--; bp++)
    	    outw_p(*bp, DT_WDATA);
#endif
	/* prime first byte onto data lines */
	STROBE(DT_CYCLE);
	nop();
	STROBE(DT_CYCLE);
	nop();
	STROBE(DT_CYCLE);
	/* issue write command */
	cmdwt(tcb,DT_WRITE);
	/* done w/ wri */
	disable_dma(CIPHER_TAPE_DMA);
	/* now check to se if write good */
	s = STATUS & (DT_FMK | DT_HER | DT_CER);
	if(!s) {
	    outw_p(0, DT_CONFIG);
	    if(STATUS & DT_OVERFLOW)
		tcb->error = TERR_DMAOVF;
	    else if(inw_p(DT_EXTENDED) & DT_EX_NOTEMPTY)
		tcb->error = TERR_DMAWR;
	    write_reset_counters(tcb);
	    return tcb->error;
	}
	printk("cipher: Error; retry [error: %x, retry %d]\n", s, j+1);
	if(j < 10) {
	    tcb->error = TERR_CORRERR;
	    cmdwt(tcb,DT_RECR);
	    cmdwt(tcb,DT_WEG);
	}
    }
    return tcb->error = TERR_NCORRERR;
}
#endif

static int purge_write(TCB *tcb)
{
    if(tcb->rwmode)
    	write_buffer(tcb);
    tcb->before = tcb->after = 0;
    return tcb->error;
}

static int cipher_sync(struct inode * inode, struct file * file)
{
    TCB *tcb = &_tcb[MINOR(inode->i_rdev) & 0x1f];
    
    if(tcb->rwmode)
    	purge_write(tcb);
    return cipher_error(tcb);
}

static int cipher_write(struct inode * inode, struct file * file,
			char * buf, int count)
{
    TCB *tcb = &_tcb[MINOR(inode->i_rdev) & 0x1f];
    int err, _count = 0;

    if(!tcb->blksiz) {
	tcb->flags |= CF_AUTOSIZE;
	tcb->blksiz = count;
    }
    do {
	err = jwrite(tcb, buf, count);
	if(err > 0) {
	    buf += err;
	    count -= err;
	    _count += err;
	}
    } while(count && err > 0);
    err = cipher_error(tcb);
    if(err)
    	return -err;
    else
    	return _count;
}

static int cipher_read(struct inode * inode, struct file * file,
		       char * buf, int count)
{
    TCB *tcb = &_tcb[MINOR(inode->i_rdev) & 0x1f];
    int err, _count = 0;

/*    do { */
	err = jread(tcb, buf, count);
	if(err > 0) {
	    buf += err;
	    count -= err;
	    _count += err;
	}
/*    } while(count && err > 0); */
    err = cipher_error(tcb);
    if(err)
    	return -err;
    else
    	return _count;
}

static int cipher_open(struct inode * inode, struct file * file)
{
    unsigned int minor = MINOR(inode->i_rdev) & 0x1f;
    unsigned char dens = MINOR(inode->i_rdev) & 0x60;
    TCB *tcb;

    if(minor > 0)
    	return -ENODEV;
    tcb = &_tcb[minor];
    if(!tcb->buffer)
    	tcb->buffer = kmalloc(64*1024, GFP_KERNEL | GFP_DMA);
    tcb->bufsiz = 64*1024;
    if(!tcb->buffer)
    	return -ENOMEM;
    if(dens) {
	if(dens == 0x40)
	    cmdwt(tcb, DT_LODEN);
	else
	    cmdwt(tcb, DT_HIDEN);
    }
    MOD_INC_USE_COUNT;
    return 0;
}

static void cipher_release(struct inode * inode, struct file * file)
{
    unsigned int minor = MINOR(inode->i_rdev) & 0x1f;
    unsigned char rew = MINOR(inode->i_rdev) & 0x80;
    TCB *tcb;

    if(minor > 0)
    	return;
    tcb = &_tcb[minor];
    purge_tbuffer(tcb);
    if((file->f_flags & O_ACCMODE) == O_WRONLY) {
	if(!select(tcb))
	    cmdwt(tcb, DT_WFM);
	tcb->fileno++;
	tcb->recno = 0;
    }
    if(!rew)
    	jrew(tcb);
    if(tcb->flags & CF_AUTOSIZE) {
    	tcb->flags &= ~CF_AUTOSIZE;
	tcb->bufsiz = 0;
    }
    MOD_DEC_USE_COUNT;
}


static int cipher_ioctl(struct inode *inode, struct file *file,
			unsigned int cmd_in, unsigned long arg)
{
    unsigned int minor = MINOR(inode->i_rdev) & 0x1f;
    struct mtop mtc;
    struct mtget st;
    struct mtpos mt_pos;
    TCB *tcb = &_tcb[minor];
    int cnt, cmd;

    if(minor > 0)
    	return -ENODEV;
    cmd = cmd_in & IOCCMD_MASK;
    switch(cmd) {
     case MTIOCTOP & IOCCMD_MASK:
	tcb->resid = 0;
	if (((cmd_in & IOCSIZE_MASK) >> IOCSIZE_SHIFT) != sizeof(mtc))
    	    return (-EINVAL);
	if((cnt = verify_area(VERIFY_READ, (void *)arg, sizeof(mtc))))
    	    return cnt;
	memcpy_fromfs((char *) &mtc, (char *)arg, sizeof(struct mtop));
	cnt = mtc.mt_count;
	switch(mtc.mt_op) {
	 case MTFSF:
	 case MTFSFM:
	    /* skip file forward */
	    if(cnt <= 0)
	    	break;
	    purge_tbuffer(tcb);
	    tcb->fileno += cnt;
	    do {
		cmdwt(tcb,DT_FILEF|DT_HISPEED);
	    } while(!(STATUS & DT_EOT) && --cnt);
	    if(cnt) {
		tcb->fileno -= cnt;
		tcb->error = TERR_EOT;
		tcb->resid = cnt;
	    } else if(mtc.mt_op == MTFSFM)
	    	cmdwt(tcb, DT_FILER|DT_HISPEED);
	    tcb->recno = 0;
	    break;
	 case MTBSF:
	 case MTBSFM:
	    /* skip file reverse */
	    if(cnt <= 0)
	    	break;
	    purge_tbuffer(tcb);
	    if(STATUS & DT_LOADPT) {
		tcb->error = TERR_REVBOT;
		break;
	    }
	    tcb->fileno -= cnt;
	    tcb->recno = -1; /* can't know pos? */
	    while(!(STATUS & DT_LOADPT) && cnt--)
	    	cmdwt(tcb,DT_RECR|DT_HISPEED);
	    if(cnt) {
		tcb->fileno += cnt;
		tcb->error = TERR_REVBOT;
	    } else if(mtc.mt_op == MTBSFM)
	    	cmdwt(tcb, DT_FILEF|DT_HISPEED);
	    break;
	 case MTFSR:
	    /* skip record forward */
	    if(cnt <= 0)
	    	break;
	    if(tcb->rwmode) {
		tcb->error = TERR_INVMODE;
		break;
	    }
	    purge_tbuffer(tcb);
	    tcb->recno += cnt;
	    do {
		cmdwt(tcb,DT_RECF|DT_HISPEED);
	    } while(!(STATUS & DT_FMK) && --cnt);
	    if(cnt) {
		tcb->recno -= cnt;
		tcb->error = TERR_EOF;
	    }
	    break;
	 case MTBSR:
	    /* skip record reverse */
	    if(cnt <= 0)
	    	break;
	    if(tcb->rwmode) {
		tcb->error = TERR_INVMODE;
		break;
	    }
	    purge_tbuffer(tcb);
	    tcb->recno -= cnt;
	    do {
		cmdwt(tcb,DT_RECF|DT_HISPEED);
	    } while(!(STATUS & DT_FMK) && --cnt);
	    if(cnt) {
		tcb->recno += cnt;
		tcb->error = TERR_EOF;
	    }
	    break;
	 case MTWEOF:
	    if(select(tcb))
	    	break;
	    if(purge_tbuffer(tcb))
	    	break;
	    /* write file mark[s] */
	    while(cnt-- && !tcb->error)
	    	if(!cmdwt(tcb,DT_WFM))
		    tcb->fileno++;
	    break;
	 case MTREW:
	    /* rewind tape */
	    jrew(tcb);
	    break;
	 case MTOFFL:
	    /* rewind and unload -- don't wait */
	    purge_tbuffer(tcb);
	    if(select(tcb))
	    	return tcb->error;
	    CMD(DT_RWU);
	    CMD(0);
	    tcb->recno = tcb->fileno = 0;
	    break;
	 case MTNOP:
	    break;
	 case MTRESET:
	    ifc_init(tcb);
	    /* then "retension" */
	 case MTRETEN:
	    /* Retension: ???  - rewind, fastforward to end, then rewind again */
	    jrew(tcb);
	    while(tcb->error == TERR_OK && !(STATUS & DT_EOT))
		cmdwt(tcb,DT_FILEF|DT_HISPEED);
	    jrew(tcb);
	    break;
	 case MTEOM:
	    while(tcb->error == TERR_OK && !(STATUS & DT_EOT))
		if(!cmdwt(tcb,DT_FILEF|DT_HISPEED))
		    tcb->fileno++;
	    break;
	 case MTERASE:
	    /* Erase??? */
	    cmdwt(tcb, DT_WEG);
	    break;
	 case MTSETBLK:
	    /* 0: autosize */
	    if(cnt) {
		if(cnt > 65536 || cnt < 0)
	    	    return -EINVAL;
		else
	    	    tcb->blksiz = cnt;
	    }
	    break;
	 case MTSETDENSITY:
	    if(cnt > 1 || cnt < 0)
	    	return -EINVAL;
	    if(cnt)
	    	cmdwt(tcb, DT_HIDEN);
	    else
	    	cmdwt(tcb, DT_LODEN);
	    break;
	 default:
	    return -ENOSYS;
	}
	memcpy_tofs((char *)arg, (char *) (&mt_pos), sizeof(struct mtpos));
	return cipher_error(tcb);
     case MTIOCGET & IOCCMD_MASK:
	/* receive status */
	if (((cmd_in & IOCSIZE_MASK) >> IOCSIZE_SHIFT) != sizeof(st))
	    return (-EINVAL);
	cnt = verify_area(VERIFY_WRITE, (void *)arg, sizeof(struct mtget));
	if(cnt)
	    return cnt;
	st.mt_type = 0; /* ok, unknown'll do for now */
	st.mt_resid = tcb->resid;
	st.mt_dsreg = STATUS;
	st.mt_erreg = tcb->error;
	st.mt_fileno = tcb->fileno;
	st.mt_blkno = tcb->recno;
	st.mt_gstat = 0;
	if(st.mt_dsreg & DT_FMK)
	    st.mt_gstat |= GMT_EOF(~0);
	if(st.mt_dsreg & DT_EOT)
	    st.mt_gstat |= GMT_EOT(~0);
	if(st.mt_dsreg & DT_LOADPT)
	    st.mt_gstat |= GMT_BOT(~0);
	if(!st.mt_dsreg & DT_FPT)
	    st.mt_gstat |= GMT_WR_PROT(~0);
	if(st.mt_dsreg & DT_ONLINE)
	    st.mt_gstat |= GMT_ONLINE(~0);
	/* format unknown */
	if(!(st.mt_dsreg & DT_NRZI))
	    st.mt_gstat |= GMT_DR_OPEN(~0);
	memcpy_tofs((char *)arg, (char *)&st, sizeof(struct mtget));
	tcb->error = 0;
	return 0;
     case MTIOCPOS & IOCCMD_MASK:
	if (((cmd_in & IOCSIZE_MASK) >> IOCSIZE_SHIFT) != sizeof(struct mtpos))
	    return (-EINVAL);
	cnt = verify_area(VERIFY_WRITE, (void *)arg, sizeof(struct mtpos));
	if(cnt)
	    return cnt;
	mt_pos.mt_blkno = tcb->recno;
	memcpy_tofs((char *)arg, (char *) (&mt_pos), sizeof(struct mtpos));
	return 0;
     default:
	return -EIO;
    }
}

static void cipher_tape_interrupt(int num, struct pt_regs *r)
{
    if(_tcb->buffer && _tcb->wait_queue)
    	wake_up(&_tcb->wait_queue);
}

static int cipher_error(TCB *tcb)
{
    if(!tcb->error)
    	return 0;
    if(tcb->error == TERR_EOF || tcb->error == TERR_BOT || tcb->error == TERR_EOT ||
       tcb->error == TERR_TRIVBLK)
    	return 0;
    return -EIO;
}

static struct file_operations cipher_fops = {
    NULL,
    cipher_read,
    cipher_write,
    NULL,
    NULL,
    cipher_ioctl,
    NULL,
    cipher_open,
    cipher_release,
    cipher_sync
};

#if CIPHER_TAPE_IFC == AT_16
#define CIPHER_PORTS 32
#else
#define CIPHER_PORTS 16
#endif

int init_cipher(void)
{
    if (register_chrdev(CT_MAJOR,"cipher",&cipher_fops)) {
	printk("cipher: unable to get major %d\n", CT_MAJOR);
	return 0;
    }
    request_region(CIPHER_TAPE_PORT, CIPHER_PORTS, "cipher");
    if (request_irq(CIPHER_TAPE_IRQ, cipher_tape_interrupt, SA_INTERRUPT, "cipher")) {
	printk("cipher: can't allocate IRQ %d\n", CIPHER_TAPE_IRQ);
	release_region(CIPHER_TAPE_PORT, CIPHER_PORTS);
	unregister_chrdev(CT_MAJOR,"cipher");
	return 0;
    }
#if CIPHER_TAPE_DMA > 4
    if (request_dma(CIPHER_TAPE_DMA,"cipher")) {
	printk("cipher: can't allocate DMA %d\n", CIPHER_TAPE_DMA);
	free_irq(CIPHER_TAPE_IRQ);
	release_region(CIPHER_TAPE_PORT, CIPHER_PORTS);
	unregister_chrdev(CT_MAJOR,"cipher");
	return 0;
    }
#endif
    printk("cipher: device @ 0x%x, IRQ %d, DMA %d\n", CIPHER_TAPE_PORT, CIPHER_TAPE_IRQ, CIPHER_TAPE_DMA);
    ifc_init(_tcb);
    return 1;
}

#ifndef MODULE

long cipher_init(long kmem_start)
{
    init_cipher();
    return kmem_start;
}

#else

char kernel_version[]= UTS_RELEASE;

int init_module(void)
{
    if(!init_cipher())
    	return -EIO;
    return 0;
}

void cleanup_module(void)
{
    if(MOD_IN_USE)
    	printk("cipher: busy - remove delayed\n");
    else {
	if(_tcb->buffer)
    	    kfree(_tcb->buffer);
	free_irq(CIPHER_TAPE_IRQ);
#if CIPHER_TAPE_DMA > 4
	free_dma(CIPHER_TAPE_DMA);
#endif
	release_region(CIPHER_TAPE_PORT, CIPHER_PORTS);
	unregister_chrdev(CT_MAJOR,"cipher");
    }
}

#endif
