[BACK]Return to fdd.c CVS log [TXT][DIR] Up to [local] / prex-old / dev / i386 / pc

File: [local] / prex-old / dev / i386 / pc / fdd.c (download)

Revision 1.1, Tue Jun 3 09:38:41 2008 UTC (15 years, 11 months ago) by nbrk
Branch point for: MAIN

Initial revision

/*-
 * Copyright (c) 2005-2006, Kohsuke Ohtani
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the names of any co-contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * fdd.c - Floppy disk driver
 */

/*
 *  State transition table:
 *
 *    State     Interrupt Timeout   Error
 *    --------- --------- --------- ---------
 *    Off       N/A       On        N/A
 *    On        N/A       Reset     N/A
 *    Reset     Recal     Off       N/A
 *    Recal     Seek      Off       Off
 *    Seek      IO        Reset     Off
 *    IO        Ready     Reset     Off
 *    Ready     N/A       Off       N/A
 *
 */

#include <driver.h>
#include <delay.h>
#include <cpu.h>

#include "dma.h"

/* #define DEBUG_FDD */

#ifdef DEBUG_FDD
#define fdd_printf(fmt, args...)	printk("%s: " fmt, ## args)
#else
#define fdd_printf(fmt...)		do {} while (0)
#endif

#define FDD_IRQ		6	/* IRQ6 */
#define FDD_DMA		2	/* DMA2 */

#define SECTOR_SIZE	512
#define TRACK_SIZE	(SECTOR_SIZE * 18)
#define INVALID_TRACK	-1

/* I/O ports */
#define FDC_DOR		0x3f2	/* Digital output register */
#define FDC_MSR		0x3f4	/* Main status register (in) */
#define FDC_DSR		0x3f4	/* Data rate select register (out) */
#define FDC_DAT		0x3f5	/* Data register */
#define FDC_DIR		0x3f7	/* Digital input register (in) */
#define FDC_CCR		0x3f7	/* Configuration control register (out) */

/* Command bytes */
#define CMD_SPECIFY	0x03	/* Specify drive timing */
#define CMD_DRVSTS	0x04
#define CMD_WRITE	0xc5	/* Sector write, multi-track */
#define CMD_READ	0xe6	/* Sector read */
#define CMD_RECAL	0x07	/* Recalibrate */
#define CMD_SENSE	0x08	/* Sense interrupt status */
#define CMD_FORMAT	0x4d	/* Format track */
#define CMD_SEEK	0x0f	/* Seek track */
#define CMD_VERSION	0x10	/* FDC version */

/* Floppy Drive Geometries */
#define FDG_HEADS	2
#define FDG_TRACKS	80
#define FDG_SECTORS	18
#define FDG_GAP3FMT	0x54
#define FDG_GAP3RW	0x1b

/* FDC state */
#define FDS_OFF		0	/* Motor off */
#define FDS_ON		1	/* Motor on */
#define FDS_RESET	2	/* Reset */
#define FDS_RECAL	3	/* Recalibrate */
#define FDS_SEEK	4	/* Seek */
#define FDS_IO		5	/* Read/write */
#define FDS_READY	6	/* Ready */

static void fdc_timeout(u_long);
static void fdc_recal(void);
static void fdc_off(void);
static void fdc_io(void);

static int fdd_init(void);
static int fdd_open(device_t, int);
static int fdd_close(device_t);
static int fdd_read(device_t, char *, size_t *, int);
static int fdd_write(device_t, char *, size_t *, int);

/*
 * I/O request
 */
struct io_req {
	int	cmd;
	int	nr_retry;
	int	blkno;
	u_long	blksz;
	void	*buf;
	int	errno;
};
typedef struct io_req *ioreq_t;

/* I/O command */
#define IO_NONE		0
#define IO_READ		1
#define IO_WRITE	2
#define IO_FORMAT	3	/* not supported */
#define IO_CANCEL	4

/*
 * Driver structure
 */
struct driver fdd_drv = {
	/* name */	"Floppy Disk Controller",
	/* order */	5,
	/* init */	fdd_init,
};

static struct devio fdd_io = {
	/* open */	fdd_open,
	/* close */	fdd_close,
	/* read */	fdd_read,
	/* write */	fdd_write,
	/* ioctl */	NULL,
	/* event */	NULL,
};

static device_t fdd_dev;	/* Device object */
static int fdd_irq;		/* Interrupt handle */
static int fdd_dma;		/* DMA handle */
static int nr_open;		/* Open count */
static struct timer fdd_tmr;	/* Timer */

static int fdc_stat;		/* Current state */
static struct io_req ioreq;	/* I/O request */
static void *read_buf;		/* DMA buffer for read (1 track) */
static void *write_buf;		/* DMA buffer for write (1 sector) */
static u_char result[7];	/* Result from fdc */
static struct event io_event = EVENT_INIT(io_event, "fdd");
static int track_cache;		/* Current track of read buffer */

/*
 * Send data to FDC
 * Return -1 on failure
 */
static int
fdc_out(u_char dat)
{
	int i;

	for (i = 0; i < 100000; i++) {
		if ((inb_p(FDC_MSR) & 0xc0) == 0x80) {
			outb_p(dat, FDC_DAT);
			return 0;
		}
	}
	printk("fdc: timeout msr=%x\n", inb(FDC_MSR));
	return -1;
}

/* Return number of result bytes */
static int
fdc_result(void)
{
	int i, msr, index = 0;

	for (i = 0; i < 50000; i++) {	/* timeout=500msec */
		msr = inb_p(FDC_MSR);
		if ((msr & 0xd0) == 0x80) {
			return index;
		}
		if ((msr & 0xd0) == 0xd0) {
			if (index > 6) {
				printk("fdc: overrun\n");
				return -1;
			}
			result[index++] = inb_p(FDC_DAT);
			/*
			fdd_printf("result[%d]=%x\n", index - 1,
				   result[index - 1]);
			*/
		}
		delay_usec(10);
	}
	printk("fdc: timeout\n");
	return -1;
}

static void
fdc_error(int errno)
{
	printk("fdc: errno=%d\n", errno);

	dma_stop(fdd_dma);
	ioreq.errno = errno;
	sched_wakeup(&io_event);
	fdc_off();
}

/*
 * Stop motor. (No interrupt)
 */
static void
fdc_off(void)
{
	fdd_printf("motor off\n");

	fdc_stat = FDS_OFF;
	timer_stop(&fdd_tmr);
	outb_p(0x0c, FDC_DOR);
}

/*
 * Start motor and wait 250msec. (No interrupt)
 */
static void
fdc_on(void)
{
	fdd_printf("motor on\n");

	fdc_stat = FDS_ON;
	outb_p(0x1c, FDC_DOR);
	timer_callout(&fdd_tmr, fdc_timeout, 0, 250);
}

/*
 * Reset FDC and wait an intterupt.
 * Timeout is 500msec.
 */
static void
fdc_reset(void)
{
	fdd_printf("reset\n");

	fdc_stat = FDS_RESET;
	timer_callout(&fdd_tmr, fdc_timeout, 0, 500);
	outb_p(0x18, FDC_DOR);	/* Motor0 enable, DMA enable */
	delay_usec(20);		/* Wait 20 usec while reset */
	outb_p(0x1c, FDC_DOR);	/* Clear reset */
}

/*
 * Recalibrate FDC and wait an interrupt.
 * Timeout is 5sec.
 */
static void
fdc_recal(void)
{
	fdd_printf("recalibrate\n");

	fdc_stat = FDS_RECAL;
	timer_callout(&fdd_tmr, fdc_timeout, 0, 5000);
	fdc_out(CMD_RECAL);
	fdc_out(0);		/* Drive 0 */
}

/*
 * Seek FDC and wait an interrupt.
 * Timeout is 4sec.
 */
static void
fdc_seek(void)
{
	u_int head, track;

	fdd_printf("seek\n");
	fdc_stat = FDS_SEEK;
	head = (ioreq.blkno % (FDG_SECTORS * FDG_HEADS)) / FDG_SECTORS;
	track = ioreq.blkno / (FDG_SECTORS * FDG_HEADS);

	timer_callout(&fdd_tmr, fdc_timeout, 0, 4000);

	fdc_out(CMD_SPECIFY);	/* specify command parameter */
	fdc_out(0xd1);		/* Step rate = 3msec, Head unload time = 16msec */
	fdc_out(0x02);		/* Head load time = 2msec, Dma on (0) */

	fdc_out(CMD_SEEK);
	fdc_out(head << 2);
	fdc_out(track);
}

/*
 * Read/write data and wait an interrupt.
 * Timeout is 2sec.
 */
static void
fdc_io(void)
{
	u_int head, track, sect;
	u_long io_size;
	int read;

	fdd_printf("read/write\n");
	fdc_stat = FDS_IO;

	head = (ioreq.blkno % (FDG_SECTORS * FDG_HEADS)) / FDG_SECTORS;
	track = ioreq.blkno / (FDG_SECTORS * FDG_HEADS);
	sect = ioreq.blkno % FDG_SECTORS + 1;
	io_size = ioreq.blksz * SECTOR_SIZE;
	read = (ioreq.cmd == IO_READ) ? 1 : 0;

	fdd_printf("hd=%x trk=%x sec=%x size=%d read=%d\n",
		   head, track, sect, io_size, read);

	timer_callout(&fdd_tmr, fdc_timeout, 0, 2000);

	dma_setup(fdd_dma, (u_long) ioreq.buf, io_size, read);

	/* Send command */
	fdc_out(read ? CMD_READ : CMD_WRITE);
	fdc_out(head << 2);
	fdc_out(track);
	fdc_out(head);
	fdc_out(sect);
	fdc_out(2);		/* sector size = 512 bytes */
	fdc_out(FDG_SECTORS);
	fdc_out(FDG_GAP3RW);
	fdc_out(0xff);
}

/*
 * Wake up iorequester.
 * FDC motor is set to off after 5sec.
 */
static void
fdc_ready(void)
{
	fdd_printf("wakeup requester\n");

	fdc_stat = FDS_READY;
	sched_wakeup(&io_event);
	timer_callout(&fdd_tmr, fdc_timeout, 0, 5000);
}

/*
 * Timeout handler
 */
static void
fdc_timeout(u_long unused)
{
	fdd_printf("fdc_stat=%d\n", fdc_stat);

	switch (fdc_stat) {
	case FDS_ON:
		fdc_reset();
		break;
	case FDS_RESET:
	case FDS_RECAL:
		printk("fdc: reset/recal timeout\n");
		fdc_error(EIO);
		break;
	case FDS_SEEK:
	case FDS_IO:
		printk("fdc: seek/io timeout retry=%d\n", ioreq.nr_retry);
		if (++ioreq.nr_retry <= 3)
			fdc_reset();
		else
			fdc_error(EIO);
		break;
	case FDS_READY:
		fdc_off();
		break;
	default:
		panic("fdc: unknown timeout\n");
	}
}

/*
 * Interrupt service routine
 * Do not change the fdc_stat in isr.
 */
static int
fdc_isr(int irq)
{
	fdd_printf("fdc_stat=%d\n", fdc_stat);

	timer_stop(&fdd_tmr);
	switch (fdc_stat) {
	case FDS_IO:
		dma_stop(fdd_dma);
		/* Fall through */
	case FDS_RESET:
	case FDS_RECAL:
	case FDS_SEEK:
		if (ioreq.cmd == IO_NONE) {
			printk("fdc: invalid interrupt\n");
			timer_stop(&fdd_tmr);
			break;
		}
		return INT_CONTINUE;
	case FDS_OFF:
		break;
	default:
		printk("fdc: unknown interrupt\n");
		break;
	}
	return 0;
}

/*
 * Interrupt service thread
 * This is called when command completion.
 */
static void
fdc_ist(int irq)
{
	int i;

	fdd_printf("fdc_stat=%d\n", fdc_stat);
	if (ioreq.cmd == IO_NONE)
		return;

	switch (fdc_stat) {
	case FDS_RESET:
		/* clear output buffer */
		for (i = 0; i < 4; i++) {
			fdc_out(CMD_SENSE);
			fdc_result();
		}
		fdc_recal();
		break;
	case FDS_RECAL:
		fdc_out(CMD_SENSE);
		fdc_result();
		if ((result[0] & 0xf8) != 0x20) {
			printk("fdc: recal error\n");
			fdc_error(EIO);
			break;
		}
		fdc_seek();
		break;
	case FDS_SEEK:
		fdc_out(CMD_SENSE);
		fdc_result();
		if ((result[0] & 0xf8) != 0x20) {
			printk("fdc: seek error\n");
			if (++ioreq.nr_retry <= 3)
				fdc_reset();
			else
				fdc_error(EIO);
			break;
		}
		fdc_io();
		break;
	case FDS_IO:
		fdc_result();
		if ((result[0] & 0xd8) != 0x00) {
			printk("fdc: i/o error st0=%x st1=%x st2=%x st3=%x retry=%d\n",
			     result[0], result[1], result[2], result[3],
			     ioreq.nr_retry);
			if (++ioreq.nr_retry <= 3)
				fdc_reset();
			else
				fdc_error(EIO);
			break;
		}
		fdd_printf("i/o complete\n");
		fdc_ready();
		break;
	case FDS_OFF:
		/* Ignore */
		break;
	default:
		ASSERT(0);
	}
	return;
}

/*
 * Open
 */
static int
fdd_open(device_t dev, int mode)
{

	nr_open++;
	fdd_printf("nr_open=%d\n", nr_open);
	return 0;
}

/*
 * Close
 */
static int
fdd_close(device_t dev)
{
	fdd_printf("dev=%x\n", dev);

	if (nr_open < 1)
		return EINVAL;
	nr_open--;
	if (nr_open == 0) {
		ioreq.cmd = IO_NONE;
		fdc_off();
	}
	return 0;
}

/*
 * Common routine for read/write
 */
static int
fdd_rw(int cmd, char *buf, u_long blksz, int blkno)
{
	int err;

	fdd_printf("cmd=%x buf=%x blksz=%d blkno=%x\n", cmd, buf, blksz, blkno);
	ioreq.cmd = cmd;
	ioreq.nr_retry = 0;
	ioreq.blkno = blkno;
	ioreq.blksz = blksz;
	ioreq.buf = buf;
	ioreq.errno = 0;

	sched_lock();
	if (fdc_stat == FDS_OFF)
		fdc_on();
	else
		fdc_seek();

	if (sched_sleep(&io_event) == SLP_INTR)
		err = EINTR;
	else
		err = ioreq.errno;

	sched_unlock();
	return err;
}

/*
 * Read
 *
 * Error:
 *  EINTR   ... Interrupted by signal
 *  EIO     ... Low level I/O error
 *  ENXIO   ... Write protected
 *  EFAULT  ... No physical memory is mapped to buffer
 */
static int
fdd_read(device_t dev, char *buf, size_t *nbyte, int blkno)
{
	char *kbuf;
	int i, track, sect, nr_sect, err;

	fdd_printf("read buf=%x nbyte=%d blkno=%x\n", buf, *nbyte, blkno);

	/* Check overrun */
	if (blkno > FDG_HEADS * FDG_TRACKS * FDG_SECTORS)
		return EIO;

	/* Translate buffer address to kernel address */
	kbuf = kmem_map(buf, *nbyte);
	if (kbuf == NULL)
		return EFAULT;

	nr_sect = *nbyte / SECTOR_SIZE;
	err = 0;
	for (i = 0; i < nr_sect; i++) {
		/* Translate the logical sector# to logical track#/sector#. */
		track = blkno / FDG_SECTORS;
		sect = blkno % FDG_SECTORS;
		/*
		 * If target sector does not exist in buffer,
		 * read 1 track (18 sectors) at once.
		 */
		if (track != track_cache) {
			err = fdd_rw(IO_READ, read_buf, FDG_SECTORS,
				     track * FDG_SECTORS);
			if (err) {
				track_cache = INVALID_TRACK;
				break;
			}
			track_cache = track;
		}
		memcpy(kbuf, (char *)read_buf + sect * SECTOR_SIZE,
		       SECTOR_SIZE);
		blkno++;
		kbuf += SECTOR_SIZE;
	}
	*nbyte = i * SECTOR_SIZE;
	return err;
}

/*
 * Write
 *
 * Error:
 *  EINTR   ... Interrupted by signal
 *  EIO     ... Low level I/O error
 *  ENXIO   ... Write protected
 *  EFAULT  ... No physical memory is mapped to buffer
 */
static int
fdd_write(device_t dev, char *buf, size_t *nbyte, int blkno)
{
	char *kbuf, *wbuf;
	int i, track, sect, nr_sect, err;

	fdd_printf("write buf=%x nbyte=%d blkno=%x\n", buf, *nbyte, blkno);

	/* Check overrun */
	if (blkno > FDG_HEADS * FDG_TRACKS * FDG_SECTORS)
		return EIO;

	/* Translate buffer address to kernel address */
	kbuf = kmem_map(buf, *nbyte);
	if (kbuf == NULL)
		return EFAULT;

	nr_sect = *nbyte / SECTOR_SIZE;
	err = 0;
	for (i = 0; i < nr_sect; i++) {
		/* Translate the logical sector# to track#/sector#. */
		track = blkno / FDG_SECTORS;
		sect = blkno % FDG_SECTORS;
		/*
		 * If target sector exists in read buffer, use it as
		 * write buffer to keep the cache cohrency.
		 */
		if (track == track_cache)
			wbuf = (char *)read_buf + sect * SECTOR_SIZE;
		else
			wbuf = write_buf;

		memcpy(wbuf, kbuf, SECTOR_SIZE);
		err = fdd_rw(IO_WRITE, wbuf, 1, blkno);
		if (err) {
			track_cache = INVALID_TRACK;
			break;
		}
		blkno++;
		kbuf += SECTOR_SIZE;
	}
	*nbyte = i * SECTOR_SIZE;

	fdd_printf("fdd_write err=%d\n", err);
	return err;
}

/*
 * Initialize
 */
static int
fdd_init(void)
{
	char *buf;
	int i;

	fdd_printf("fdd_init\n");
	if (inb(FDC_MSR) == 0xff) {
		printk("Floppy drive not found!\n");
		return -1;
	}

	/* Create device object */
	fdd_dev = device_create(&fdd_io, "fd0", DF_BLK);
	ASSERT(fdd_dev);

	/*
	 * Allocate physical pages for DMA buffer.
	 * Buffer: 1 track for read, 1 sector for write.
	 */
	buf = dma_alloc(TRACK_SIZE + SECTOR_SIZE);

	ASSERT(buf);
	read_buf = buf;
	write_buf = buf + TRACK_SIZE;

	/* Allocate DMA */
	fdd_dma = dma_attach(FDD_DMA);
	ASSERT(fdd_dma != -1);

	/* Allocate IRQ */
	fdd_irq = irq_attach(FDD_IRQ, IPL_BLOCK, 0, fdc_isr, fdc_ist);
	ASSERT(fdd_irq != -1);

	timer_init(&fdd_tmr);
	fdc_stat = FDS_OFF;
	ioreq.cmd = IO_NONE;
	track_cache = INVALID_TRACK;

	/* Reset FDC */
	outb_p(0x08, FDC_DOR);
	delay_usec(20);
	outb_p(0x0C, FDC_DOR);

	/* Data rate 500kbps */
	outb_p(0x00, FDC_CCR);

	/* clear output buffer */
	for (i = 0; i < 4; i++) {
		fdc_out(CMD_SENSE);
		fdc_result();
	}
	return 0;
}