root/sys/dev/qbus/rl.c
/*      $NetBSD: rl.c,v 1.53 2021/08/07 16:19:15 thorpej Exp $  */

/*
 * Copyright (c) 2000 Ludd, University of Lule}, Sweden. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 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.
 */

/*
 * RL11/RLV11/RLV12 disk controller driver and
 * RL01/RL02 disk device driver.
 *
 * TODO:
 *      Handle disk errors more gracefully
 *      Do overlapping seeks on multiple drives
 *
 * Implementation comments:
 *
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: rl.c,v 1.53 2021/08/07 16:19:15 thorpej Exp $");

#include <sys/param.h>
#include <sys/device.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/disk.h>
#include <sys/disklabel.h>
#include <sys/buf.h>
#include <sys/bufq.h>
#include <sys/stat.h>
#include <sys/dkio.h>
#include <sys/fcntl.h>
#include <sys/event.h>

#include <ufs/ufs/dinode.h>
#include <ufs/ffs/fs.h>

#include <sys/bus.h>

#include <dev/qbus/ubavar.h>
#include <dev/qbus/rlreg.h>
#include <dev/qbus/rlvar.h>

#include "ioconf.h"
#include "locators.h"

static  int rlcmatch(device_t, cfdata_t, void *);
static  void rlcattach(device_t, device_t, void *);
static  int rlcprint(void *, const char *);
static  void rlcintr(void *);
static  int rlmatch(device_t, cfdata_t, void *);
static  void rlattach(device_t, device_t, void *);
static  void rlcstart(struct rlc_softc *, struct buf *);
static  void waitcrdy(struct rlc_softc *);
static  void rlcreset(device_t);

CFATTACH_DECL_NEW(rlc, sizeof(struct rlc_softc),
    rlcmatch, rlcattach, NULL, NULL);

CFATTACH_DECL_NEW(rl, sizeof(struct rl_softc),
    rlmatch, rlattach, NULL, NULL);

static dev_type_open(rlopen);
static dev_type_close(rlclose);
static dev_type_read(rlread);
static dev_type_write(rlwrite);
static dev_type_ioctl(rlioctl);
static dev_type_strategy(rlstrategy);
static dev_type_dump(rldump);
static dev_type_size(rlpsize);

const struct bdevsw rl_bdevsw = {
        .d_open = rlopen,
        .d_close = rlclose,
        .d_strategy = rlstrategy,
        .d_ioctl = rlioctl,
        .d_dump = rldump,
        .d_psize = rlpsize,
        .d_discard = nodiscard,
        .d_flag = D_DISK
};

const struct cdevsw rl_cdevsw = {
        .d_open = rlopen,
        .d_close = rlclose,
        .d_read = rlread,
        .d_write = rlwrite,
        .d_ioctl = rlioctl,
        .d_stop = nostop,
        .d_tty = notty,
        .d_poll = nopoll,
        .d_mmap = nommap,
        .d_kqfilter = nokqfilter,
        .d_discard = nodiscard,
        .d_flag = D_DISK
};

#define MAXRLXFER (RL_BPS * RL_SPT)

#define RL_WREG(reg, val) \
        bus_space_write_2(sc->sc_iot, sc->sc_ioh, (reg), (val))
#define RL_RREG(reg) \
        bus_space_read_2(sc->sc_iot, sc->sc_ioh, (reg))

static const char * const rlstates[] = {
        "drive not loaded",
        "drive spinning up",
        "drive brushes out",
        "drive loading heads",
        "drive seeking",
        "drive ready",
        "drive unloading heads",
        "drive spun down",
};

static const struct dkdriver rldkdriver = {
        .d_strategy = rlstrategy,
        .d_minphys = minphys
};

static const char *
rlstate(struct rlc_softc *sc, int unit)
{
        int i = 0;

        do {
                RL_WREG(RL_DA, RLDA_GS);
                RL_WREG(RL_CS, RLCS_GS|(unit << RLCS_USHFT));
                waitcrdy(sc);
        } while (((RL_RREG(RL_CS) & RLCS_ERR) != 0) && i++ < 10);
        if (i == 10)
                return NULL;
        i = RL_RREG(RL_MP) & RLMP_STATUS;
        return rlstates[i];
}

void
waitcrdy(struct rlc_softc *sc)
{
        int i;

        for (i = 0; i < 1000; i++) {
                DELAY(10000);
                if (RL_RREG(RL_CS) & RLCS_CRDY)
                        return;
        }
        aprint_error_dev(sc->sc_dev, "never got ready\n"); /* ?panic? */
}

int
rlcprint(void *aux, const char *name)
{
        struct rlc_attach_args *ra = aux;

        if (name)
                aprint_normal("RL0%d at %s",
                    ra->type & RLMP_DT ? '2' : '1', name);
        aprint_normal(" drive %d", ra->hwid);
        return UNCONF;
}

/*
 * Force the controller to interrupt.
 */
int
rlcmatch(device_t parent, cfdata_t cf, void *aux)
{
        struct uba_attach_args *ua = aux;
        struct rlc_softc ssc, *sc = &ssc;
        int i;

        sc->sc_iot = ua->ua_iot;
        sc->sc_ioh = ua->ua_ioh;
        /* Force interrupt by issuing a "Get Status" command */
        RL_WREG(RL_DA, RLDA_GS);
        RL_WREG(RL_CS, RLCS_GS|RLCS_IE);

        for (i = 0; i < 100; i++) {
                DELAY(100000);
                if (RL_RREG(RL_CS) & RLCS_CRDY)
                        return 1;
        }
        return 0;
}

void
rlcattach(device_t parent, device_t self, void *aux)
{
        struct rlc_softc *sc = device_private(self);
        struct uba_attach_args *ua = aux;
        struct rlc_attach_args ra;
        int i, error;

        sc->sc_dev = self;
        sc->sc_uh = device_private(parent);
        sc->sc_iot = ua->ua_iot;
        sc->sc_ioh = ua->ua_ioh;
        sc->sc_dmat = ua->ua_dmat;
        uba_intr_establish(ua->ua_icookie, ua->ua_cvec,
                rlcintr, sc, &sc->sc_intrcnt);
        evcnt_attach_dynamic(&sc->sc_intrcnt, EVCNT_TYPE_INTR, ua->ua_evcnt,
                device_xname(sc->sc_dev), "intr");
        uba_reset_establish(rlcreset, self);

        printf("\n");

        /*
         * The RL11 can only have one transfer going at a time,
         * and max transfer size is one track, so only one dmamap
         * is needed.
         */
        error = bus_dmamap_create(sc->sc_dmat, MAXRLXFER, 1, MAXRLXFER, 0,
            BUS_DMA_ALLOCNOW, &sc->sc_dmam);
        if (error) {
                aprint_error(": Failed to allocate DMA map, error %d\n", error);
                return;
        }
        bufq_alloc(&sc->sc_q, "disksort", BUFQ_SORT_CYLINDER);
        for (i = 0; i < RL_MAXDPC; i++) {
                waitcrdy(sc);
                RL_WREG(RL_DA, RLDA_GS|RLDA_RST);
                RL_WREG(RL_CS, RLCS_GS|(i << RLCS_USHFT));
                waitcrdy(sc);
                ra.type = RL_RREG(RL_MP);
                ra.hwid = i;
                if ((RL_RREG(RL_CS) & RLCS_ERR) == 0)
                        config_found(sc->sc_dev, &ra, rlcprint, CFARGS_NONE);
        }
}

int
rlmatch(device_t parent, cfdata_t cf, void *aux)
{
        struct rlc_attach_args *ra = aux;

        if (cf->cf_loc[RLCCF_DRIVE] != RLCCF_DRIVE_DEFAULT &&
            cf->cf_loc[RLCCF_DRIVE] != ra->hwid)
                return 0;
        return 1;
}

void
rlattach(device_t parent, device_t self, void *aux)
{
        struct rl_softc *rc = device_private(self);
        struct rlc_attach_args *ra = aux;
        struct disklabel *dl;

        rc->rc_dev = self;
        rc->rc_rlc = device_private(parent);
        rc->rc_hwid = ra->hwid;
        disk_init(&rc->rc_disk, device_xname(rc->rc_dev), &rldkdriver);
        disk_attach(&rc->rc_disk);
        dl = rc->rc_disk.dk_label;
        dl->d_npartitions = 3;
        strcpy(dl->d_typename, "RL01");
        if (ra->type & RLMP_DT)
                dl->d_typename[3] = '2';
        dl->d_secsize = DEV_BSIZE; /* XXX - wrong, but OK for now */
        dl->d_nsectors = RL_SPT/2;
        dl->d_ntracks = RL_SPD;
        dl->d_ncylinders = ra->type & RLMP_DT ? RL_TPS02 : RL_TPS01;
        dl->d_secpercyl = dl->d_nsectors * dl->d_ntracks;
        dl->d_secperunit = dl->d_ncylinders * dl->d_secpercyl;
        dl->d_partitions[0].p_size = dl->d_partitions[2].p_size =
            dl->d_secperunit;
        dl->d_partitions[0].p_offset = dl->d_partitions[2].p_offset = 0;
        dl->d_interleave = dl->d_headswitch = 1;
        dl->d_bbsize = BBSIZE;
        dl->d_sbsize = SBLOCKSIZE;
        dl->d_rpm = 2400;
        dl->d_type = DKTYPE_DEC;
        printf(": %s, %s\n", dl->d_typename, rlstate(rc->rc_rlc, ra->hwid));

        /*
         * XXX We should try to discovery wedges here, but
         * XXX that would mean loading up the pack and being
         * XXX able to do I/O.  Should use config_defer() here.
         */
}

int
rlopen(dev_t dev, int flag, int fmt, struct lwp *l)
{
        struct rl_softc * const rc = device_lookup_private(&rl_cd, DISKUNIT(dev));
        struct rlc_softc *sc;
        int error, part, mask;
        struct disklabel *dl;
        const char *msg;

        /*
         * Make sure this is a reasonable open request.
         */
        if (rc == NULL)
                return ENXIO;

        sc = rc->rc_rlc;
        part = DISKPART(dev);

        mutex_enter(&rc->rc_disk.dk_openlock);

        /*
         * If there are wedges, and this is not RAW_PART, then we
         * need to fail.
         */
        if (rc->rc_disk.dk_nwedges != 0 && part != RAW_PART) {
                error = EBUSY;
                goto bad1;
        }

        /* Check that the disk actually is useable */
        msg = rlstate(sc, rc->rc_hwid);
        if (msg == NULL || msg == rlstates[RLMP_UNLOAD] ||
            msg == rlstates[RLMP_SPUNDOWN]) {
                error = ENXIO;
                goto bad1;
        }
        /*
         * If this is the first open; read in where on the disk we are.
         */
        dl = rc->rc_disk.dk_label;
        if (rc->rc_state == DK_CLOSED) {
                u_int16_t mp;
                int maj;
                RL_WREG(RL_CS, RLCS_RHDR|(rc->rc_hwid << RLCS_USHFT));
                waitcrdy(sc);
                mp = RL_RREG(RL_MP);
                rc->rc_head = ((mp & RLMP_HS) == RLMP_HS);
                rc->rc_cyl = (mp >> 7) & 0777;
                rc->rc_state = DK_OPEN;
                /* Get disk label */
                maj = cdevsw_lookup_major(&rl_cdevsw);
                if ((msg = readdisklabel(MAKEDISKDEV(maj,
                    device_unit(rc->rc_dev), RAW_PART), rlstrategy, dl, NULL)))
                        aprint_normal_dev(rc->rc_dev, "%s", msg);
                aprint_normal_dev(rc->rc_dev, "size %d sectors\n",
                    dl->d_secperunit);
        }
        if (part >= dl->d_npartitions) {
                error = ENXIO;
                goto bad1;
        }

        mask = 1 << part;
        switch (fmt) {
        case S_IFCHR:
                rc->rc_disk.dk_copenmask |= mask;
                break;
        case S_IFBLK:
                rc->rc_disk.dk_bopenmask |= mask;
                break;
        }
        rc->rc_disk.dk_openmask |= mask;
        error = 0;
 bad1:
        mutex_exit(&rc->rc_disk.dk_openlock);
        return (error);
}

int
rlclose(dev_t dev, int flag, int fmt, struct lwp *l)
{
        int unit = DISKUNIT(dev);
        struct rl_softc *rc = device_lookup_private(&rl_cd, unit);
        int mask = (1 << DISKPART(dev));

        mutex_enter(&rc->rc_disk.dk_openlock);

        switch (fmt) {
        case S_IFCHR:
                rc->rc_disk.dk_copenmask &= ~mask;
                break;
        case S_IFBLK:
                rc->rc_disk.dk_bopenmask &= ~mask;
                break;
        }
        rc->rc_disk.dk_openmask =
            rc->rc_disk.dk_copenmask | rc->rc_disk.dk_bopenmask;

        if (rc->rc_disk.dk_openmask == 0)
                rc->rc_state = DK_CLOSED; /* May change pack */
        mutex_exit(&rc->rc_disk.dk_openlock);
        return 0;
}

void
rlstrategy(struct buf *bp)
{
        struct rl_softc * const rc = device_lookup_private(&rl_cd, DISKUNIT(bp->b_dev));
        struct disklabel *lp;
        int s;

        if (rc == NULL || rc->rc_state != DK_OPEN) /* How did we end up here at all? */
                panic("rlstrategy: state impossible");

        lp = rc->rc_disk.dk_label;
        if (bounds_check_with_label(&rc->rc_disk, bp, 1) <= 0)
                goto done;

        if (bp->b_bcount == 0)
                goto done;

        bp->b_rawblkno =
            bp->b_blkno + lp->d_partitions[DISKPART(bp->b_dev)].p_offset;
        bp->b_cylinder = bp->b_rawblkno / lp->d_secpercyl;

        s = splbio();
        bufq_put(rc->rc_rlc->sc_q, bp);
        rlcstart(rc->rc_rlc, 0);
        splx(s);
        return;

done:   biodone(bp);
}

int
rlioctl(dev_t dev, u_long cmd, void *addr, int flag, struct lwp *l)
{
        struct rl_softc *rc = device_lookup_private(&rl_cd, DISKUNIT(dev));
        struct disklabel *lp = rc->rc_disk.dk_label;
        int error;
#ifdef __HAVE_OLD_DISKLABEL
        struct disklabel newlabel;
#endif

        error = disk_ioctl(&rc->rc_disk, dev, cmd, addr, flag, l);
        if (error != EPASSTHROUGH)
                return error;
        else
                error = 0;

        switch (cmd) {
        case DIOCSDINFO:
        case DIOCWDINFO:
#ifdef __HAVE_OLD_DISKLABEL
        case ODIOCWDINFO:
        case ODIOCSDINFO:
#endif
        {
                struct disklabel *tp;

#ifdef __HAVE_OLD_DISKLABEL
                if (cmd == ODIOCSDINFO || cmd == ODIOCWDINFO) {
                        memset(&newlabel, 0, sizeof newlabel);
                        memcpy(&newlabel, addr, sizeof (struct olddisklabel));
                        tp = &newlabel;
                } else
#endif
                tp = (struct disklabel *)addr;

                if ((flag & FWRITE) == 0)
                        error = EBADF;
                else {
                        mutex_enter(&rc->rc_disk.dk_openlock);
                        error = ((
#ifdef __HAVE_OLD_DISKLABEL
                               cmd == ODIOCSDINFO ||
#endif
                               cmd == DIOCSDINFO) ?
                            setdisklabel(lp, tp, 0, 0) :
                            writedisklabel(dev, rlstrategy, lp, 0));
                        mutex_exit(&rc->rc_disk.dk_openlock);
                }
                break;
        }

        case DIOCWLABEL:
                if ((flag & FWRITE) == 0)
                        error = EBADF;
                break;

        default:
                error = ENOTTY;
                break;
        }
        return error;
}

int
rlpsize(dev_t dev)
{
        struct rl_softc * const rc = device_lookup_private(&rl_cd, DISKUNIT(dev));
        struct disklabel *dl;
        int size;

        if (rc == NULL)
                return -1;
        dl = rc->rc_disk.dk_label;
        size = dl->d_partitions[DISKPART(dev)].p_size *
            (dl->d_secsize / DEV_BSIZE);
        return size;
}

int
rldump(dev_t dev, daddr_t blkno, void *va, size_t size)
{
        /* Not likely... */
        return 0;
}

int
rlread(dev_t dev, struct uio *uio, int ioflag)
{
        return (physio(rlstrategy, NULL, dev, B_READ, minphys, uio));
}

int
rlwrite(dev_t dev, struct uio *uio, int ioflag)
{
        return (physio(rlstrategy, NULL, dev, B_WRITE, minphys, uio));
}

static const char * const rlerr[] = {
        "no",
        "operation incomplete",
        "read data CRC",
        "header CRC",
        "data late",
        "header not found",
        "",
        "",
        "non-existent memory",
        "memory parity error",
        "",
        "",
        "",
        "",
        "",
        "",
};

void
rlcintr(void *arg)
{
        struct rlc_softc *sc = arg;
        struct buf *bp;
        u_int16_t cs;

        bp = sc->sc_active;
        if (bp == 0) {
                aprint_error_dev(sc->sc_dev, "strange interrupt\n");
                return;
        }
        bus_dmamap_unload(sc->sc_dmat, sc->sc_dmam);
        sc->sc_active = 0;
        cs = RL_RREG(RL_CS);
        if (cs & RLCS_ERR) {
                int error = (cs & RLCS_ERRMSK) >> 10;

                aprint_error_dev(sc->sc_dev, "%s\n", rlerr[error]);
                bp->b_error = EIO;
                bp->b_resid = bp->b_bcount;
                sc->sc_bytecnt = 0;
        }
        if (sc->sc_bytecnt == 0) /* Finished transfer */
                biodone(bp);
        rlcstart(sc, sc->sc_bytecnt ? bp : 0);
}

/*
 * Start routine. First position the disk to the given position,
 * then start reading/writing. An optimization would be to be able
 * to handle overlapping seeks between disks.
 */
void
rlcstart(struct rlc_softc *sc, struct buf *ob)
{
        struct disklabel *lp;
        struct rl_softc *rc;
        struct buf *bp;
        int bn, cn, sn, tn, blks, err;

        if (sc->sc_active)
                return; /* Already doing something */

        if (ob == 0) {
                bp = bufq_get(sc->sc_q);
                if (bp == NULL)
                        return; /* Nothing to do */
                sc->sc_bufaddr = bp->b_data;
                sc->sc_diskblk = bp->b_rawblkno;
                sc->sc_bytecnt = bp->b_bcount;
                bp->b_resid = 0;
        } else
                bp = ob;
        sc->sc_active = bp;

        rc = device_lookup_private(&rl_cd, DISKUNIT(bp->b_dev));
        bn = sc->sc_diskblk;
        lp = rc->rc_disk.dk_label;
        if (bn) {
                cn = bn / lp->d_secpercyl;
                sn = bn % lp->d_secpercyl;
                tn = sn / lp->d_nsectors;
                sn = sn % lp->d_nsectors;
        } else
                cn = sn = tn = 0;

        /*
         * Check if we have to position disk first.
         */
        if (rc->rc_cyl != cn || rc->rc_head != tn) {
                u_int16_t da = RLDA_SEEK;
                if (cn > rc->rc_cyl)
                        da |= ((cn - rc->rc_cyl) << RLDA_CYLSHFT) | RLDA_DIR;
                else
                        da |= ((rc->rc_cyl - cn) << RLDA_CYLSHFT);
                if (tn)
                        da |= RLDA_HSSEEK;
                waitcrdy(sc);
                RL_WREG(RL_DA, da);
                RL_WREG(RL_CS, RLCS_SEEK | (rc->rc_hwid << RLCS_USHFT));
                waitcrdy(sc);
                rc->rc_cyl = cn;
                rc->rc_head = tn;
        }
        RL_WREG(RL_DA, (cn << RLDA_CYLSHFT) | (tn ? RLDA_HSRW : 0) | (sn << 1));
        blks = sc->sc_bytecnt/DEV_BSIZE;

        if (sn + blks > RL_SPT/2)
                blks = RL_SPT/2 - sn;
        RL_WREG(RL_MP, -(blks*DEV_BSIZE)/2);
        err = bus_dmamap_load(sc->sc_dmat, sc->sc_dmam, sc->sc_bufaddr,
            (blks*DEV_BSIZE), (bp->b_flags & B_PHYS ? bp->b_proc : 0),
            BUS_DMA_NOWAIT);
        if (err)
                panic("%s: bus_dmamap_load failed: %d",
                    device_xname(sc->sc_dev), err);
        RL_WREG(RL_BA, (sc->sc_dmam->dm_segs[0].ds_addr & 0xffff));

        /* Count up vars */
        sc->sc_bufaddr = (char *)sc->sc_bufaddr + (blks*DEV_BSIZE);
        sc->sc_diskblk += blks;
        sc->sc_bytecnt -= (blks*DEV_BSIZE);

        if (bp->b_flags & B_READ)
                RL_WREG(RL_CS, RLCS_IE|RLCS_RD|(rc->rc_hwid << RLCS_USHFT));
        else
                RL_WREG(RL_CS, RLCS_IE|RLCS_WD|(rc->rc_hwid << RLCS_USHFT));
}

/*
 * Called once per controller when an ubareset occurs.
 * Retracts all disks and restarts active transfers.
 */
void
rlcreset(device_t dev)
{
        struct rlc_softc *sc = device_private(dev);
        struct rl_softc *rc;
        int i;
        u_int16_t mp;

        for (i = 0; i < rl_cd.cd_ndevs; i++) {
                if ((rc = device_lookup_private(&rl_cd, i)) == NULL)
                        continue;
                if (rc->rc_state != DK_OPEN)
                        continue;
                if (rc->rc_rlc != sc)
                        continue;

                RL_WREG(RL_CS, RLCS_RHDR|(rc->rc_hwid << RLCS_USHFT));
                waitcrdy(sc);
                mp = RL_RREG(RL_MP);
                rc->rc_head = ((mp & RLMP_HS) == RLMP_HS);
                rc->rc_cyl = (mp >> 7) & 0777;
        }
        if (sc->sc_active == 0)
                return;

        bufq_put(sc->sc_q, sc->sc_active);
        sc->sc_active = 0;
        rlcstart(sc, 0);
}