/*
 * ch375_hw.h - WCH CH375 (parallel bus mode) USB mass-storage protocol.
 *
 * Shared by the Linux module (ch375.c) and the userspace simulator test
 * (sim/ch375sim.c).  The includer must define:
 *
 *   CH_INB(port)        read a byte from an I/O port
 *   CH_OUTB(val, port)  write a byte to an I/O port
 *   CH_MDELAY(ms)       busy-wait for ms milliseconds
 *   CH_LOG(args)        printf-style logging, args in double parentheses
 *
 * The command sequences, timeouts and recovery steps follow FreddyV's DOS
 * driver CH375 V0.22 (C375V88.SYS), recovered by disassembly; the offsets
 * in the comments point into that binary.  Register (variable) meanings
 * marked "inferred" are taken from how the DOS driver uses them.
 */

#ifndef CH375_HW_H
#define CH375_HW_H

/* commands, written to the command port (base + 1) */
#define CH_CMD_GET_IC_VER       0x01
#define CH_CMD_CHECK_EXIST      0x06
#define CH_CMD_READ_VAR8        0x0A
#define CH_CMD_WRITE_VAR8       0x0B
#define CH_CMD_SET_USB_MODE     0x15
#define CH_CMD_ABORT_NAK        0x17
#define CH_CMD_GET_STATUS       0x22
#define CH_CMD_RD_USB_DATA      0x28
#define CH_CMD_WR_USB_DATA7     0x2B
#define CH_CMD_SET_TOGGLE       0x41    /* inferred: endpoint toggle resync */
#define CH_CMD_DISK_INIT        0x51
#define CH_CMD_DISK_RESET       0x52
#define CH_CMD_DISK_SIZE        0x53
#define CH_CMD_DISK_READ        0x54
#define CH_CMD_DISK_RD_GO       0x55
#define CH_CMD_DISK_WRITE       0x56
#define CH_CMD_DISK_WR_GO       0x57
#define CH_CMD_DISK_INQUIRY     0x58
#define CH_CMD_DISK_READY       0x59
#define CH_CMD_DISK_R_SENSE     0x5A

/* interrupt status codes, returned by GET_STATUS */
#define CH_INT_SUCCESS          0x14
#define CH_INT_DISK_READ        0x1D
#define CH_INT_DISK_WRITE       0x1E

/* chip variables used by the DOS driver */
#define CH_VAR_FLAGS            0x1B    /* driver writes 0x80; low 7 bits != 0 once a disk is mounted (inferred) */
#define CH_VAR_STATE            0x20    /* bit 5: USB device attached (inferred) */
#define CH_VAR_OLDCHIP          0x25    /* set to 0x8F on old chips */
#define CH_VAR_TOGGLE           0x31
#define CH_VAR_TOGGLE_IN        0x32
#define CH_VAR_TOGGLE_OUT       0x33
#define CH_VAR_INQ_FLAG         0x34
#define CH_VAR_INQ_LEN          0x38

#define CH_STATE_ATTACHED       0x20

/* polls of the interrupt bit before a transfer times out (DOS: 0xffff * 100) */
#define CH_POLL_LIMIT           6553500L
#define CH_CHUNK                64      /* bytes per USB data packet */
#define CH_MAX_SECTORS          255     /* sector count is one byte */

struct ch375 {
        unsigned int base;      /* data port; command/status port is base + 1 */
        int speed;              /* DOS "%" parameter: port 0x61 reads per delay unit */
        int mode;               /* 0: old chip, 1: 0xA2..0xB4, 2: >= 0xB5 (status on base + 1) */
        int dirty;              /* a write was issued, wait for DISK_READY before the next access */
        unsigned char version;
        unsigned long sectors;  /* from DISK_SIZE */
        unsigned long blksize;
        unsigned char data[CH_CHUNK];   /* last RD_USB_DATA result */
        int datalen;
        unsigned long timeouts;
        unsigned long retries;
};

/* 0x6FF: one delay unit, "speed" dummy reads of port 0x61 */
static void ch_pause(struct ch375 *c)
{
        int i;

        for (i = 0; i < c->speed; i++)
                (void) CH_INB(0x61);
}

/* 0x710 */
static void ch_pause10(struct ch375 *c)
{
        int i;

        for (i = 0; i < 10; i++)
                ch_pause(c);
}

/*
 * 0xB16: the DOS driver waits n * 1000 delay units, which is nothing at
 * speed 0.  These waits are only on the init and error paths, so use a
 * real delay instead (about the DOS length at speed 1).
 */
static void ch_wait(struct ch375 *c, int n)
{
        CH_MDELAY(n);
}

/* 0x71B: write a command, with the pre-command quirk for 0xA2..0xB4 chips */
static void ch_cmd(struct ch375 *c, unsigned char cmd)
{
        if (c->mode == 1) {
                ch_pause(c);
                CH_OUTB(CH_CMD_GET_IC_VER, c->base + 1);
                ch_pause(c);
        }
        ch_pause(c);
        ch_pause(c);
        CH_OUTB(cmd, c->base + 1);
        ch_pause(c);
        ch_pause(c);
        if (c->mode == 0)
                ch_pause(c);
}

/* 0x759 */
static void ch_wr(struct ch375 *c, unsigned char val)
{
        CH_OUTB(val, c->base);
        ch_pause(c);
}

/* 0x75E */
static unsigned char ch_rd(struct ch375 *c)
{
        ch_pause(c);
        return CH_INB(c->base);
}

/* 0x765: INT# is reported active low in bit 7 */
static int ch_int_pending(struct ch375 *c)
{
        return !(CH_INB(c->mode < 2 ? c->base : c->base + 1) & 0x80);
}

/*
 * 0x774: wait for the chip interrupt and fetch its status.  Returns the
 * status, or -1 on timeout (after aborting the stuck USB transaction).
 */
static int ch_wait_int(struct ch375 *c)
{
        long n;
        int st;

        for (n = 0; n < CH_POLL_LIMIT; n++)
                if (ch_int_pending(c))
                        break;
        if (n == CH_POLL_LIMIT) {
                ch_cmd(c, CH_CMD_ABORT_NAK);
                c->timeouts++;
                ch_wait(c, 2);
        }
        CH_OUTB(CH_CMD_GET_STATUS, c->base + 1);
        st = CH_INB(c->base);
        return n == CH_POLL_LIMIT ? -1 : st;
}

/* 0xA9A / 0xA8F: issue a command (with optional data byte) and expect success */
static int ch_cmd_ok(struct ch375 *c, unsigned char cmd)
{
        ch_cmd(c, cmd);
        return ch_wait_int(c) == CH_INT_SUCCESS;
}

static int ch_cmd1_ok(struct ch375 *c, unsigned char cmd, unsigned char val)
{
        ch_cmd(c, cmd);
        ch_wr(c, val);
        return ch_wait_int(c) == CH_INT_SUCCESS;
}

/* 0xA6B */
static unsigned char ch_read_var(struct ch375 *c, unsigned char addr)
{
        ch_cmd(c, CH_CMD_READ_VAR8);
        ch_wr(c, addr);
        return ch_rd(c);
}

/* 0xA7C */
static void ch_write_var(struct ch375 *c, unsigned char addr, unsigned char val)
{
        ch_cmd(c, CH_CMD_WRITE_VAR8);
        ch_wr(c, addr);
        ch_wr(c, val);
}

/* 0xA4D: read the USB data buffer into c->data */
static int ch_rd_data(struct ch375 *c)
{
        int i, len;

        ch_cmd(c, CH_CMD_RD_USB_DATA);
        len = ch_rd(c);
        c->datalen = 0;
        if (len > CH_CHUNK)
                return -1;
        for (i = 0; i < len; i++)
                c->data[i] = ch_rd(c);
        c->datalen = len;
        return len;
}

/* 0xB25: GET_STATUS clears a pending interrupt, then test the attach bit */
static int ch_attached(struct ch375 *c)
{
        ch_cmd(c, CH_CMD_GET_STATUS);
        (void) ch_rd(c);
        return ch_read_var(c, CH_VAR_STATE) & CH_STATE_ATTACHED;
}

/* 0xA32 */
static int ch_mounted(struct ch375 *c)
{
        return ch_read_var(c, CH_VAR_FLAGS) & 0x7F;
}

/* 0xAAB */
static void ch_sense(struct ch375 *c)
{
        if (ch_attached(c))
                (void) ch_cmd_ok(c, CH_CMD_DISK_R_SENSE);
}

/* 0xAB9 */
static int ch_ready(struct ch375 *c)
{
        return ch_cmd_ok(c, CH_CMD_DISK_READY);
}

/* 0x7AF: write 0x55 and 0xAA, the chip answers with the complement */
static int ch_check_exist(struct ch375 *c)
{
        ch_cmd(c, CH_CMD_CHECK_EXIST);
        ch_wr(c, 0x55);
        if (ch_rd(c) != 0xAA)
                return 0;
        ch_cmd(c, CH_CMD_CHECK_EXIST);
        ch_wr(c, 0xAA);
        return ch_rd(c) == 0x55;
}

/* 0x7EA: switch to USB host mode and pick the bus mode from the chip version */
static int ch_reset_init(struct ch375 *c)
{
        int ret;

        c->mode = 0;
        ch_pause10(c);
        ch_write_var(c, CH_VAR_FLAGS, 0x80);
        ch_cmd(c, CH_CMD_SET_USB_MODE);
        ch_wr(c, 6);            /* host mode, SOF generation on */
        ch_wait(c, 1);
        ret = ch_rd(c);
        ch_pause10(c);

        c->mode = 1;
        ch_cmd(c, CH_CMD_GET_IC_VER);
        c->version = ch_rd(c);
        if (c->version >= 0xB5 && c->version < 0xC0)
                c->mode = 2;
        else if (c->version >= 0xA2 && c->version < 0xC0)
                c->mode = 1;
        else {
                c->mode = 0;
                ch_write_var(c, CH_VAR_OLDCHIP, 0x8F);
        }
        ch_wait(c, 1);
        return ret;
}

/* 0xB75: DISK_SIZE, retried with REQUEST SENSE in between */
static int ch_disk_size(struct ch375 *c)
{
        int i;

        for (i = 0; i < 6; i++) {
                if (ch_cmd_ok(c, CH_CMD_DISK_SIZE)) {
                        /* raw READ CAPACITY data: last LBA, block length (big endian) */
                        if (ch_rd_data(c) >= 8) {
                                c->sectors = ((unsigned long) c->data[0] << 24 |
                                        (unsigned long) c->data[1] << 16 |
                                        (unsigned long) c->data[2] << 8 | c->data[3]) + 1;
                                c->blksize = (unsigned long) c->data[4] << 24 |
                                        (unsigned long) c->data[5] << 16 |
                                        (unsigned long) c->data[6] << 8 | c->data[7];
                        }
                        return 1;
                }
                ch_wait(c, 10);
                ch_sense(c);
                ch_wait(c, 50);
        }
        return 0;
}

/* 0xB9F / 0xB30: initialise the attached USB disk */
static int ch_mount(struct ch375 *c)
{
        if (!ch_attached(c))
                return 0;
        if (!ch_cmd_ok(c, CH_CMD_DISK_INIT))
                return 0;
        if (ch_read_var(c, CH_VAR_INQ_LEN)) {
                (void) ch_rd_data(c);
                if (c->datalen > 0 && c->data[0] == 5) {
                        ch_write_var(c, CH_VAR_INQ_FLAG, 1);
                        if (!ch_cmd_ok(c, CH_CMD_DISK_INQUIRY))
                                return 0;
                }
        }
        if (!ch_disk_size(c))
                return 0;
        if (!ch_ready(c)) {
                ch_wait(c, 20);
                ch_sense(c);
                ch_wait(c, 20);
                if (!ch_ready(c)) {
                        ch_wait(c, 20);
                        ch_sense(c);
                }
        }
        return 1;
}

/*
 * 0x84A: called before every transfer.  Mounts the disk if the chip lost
 * it, and after a write waits until the disk reports ready again.
 */
static int ch_prepare(struct ch375 *c)
{
        int i;

        if (!ch_mounted(c)) {
                c->dirty = 0;
                ch_pause10(c);
                if (!ch_attached(c))
                        return 0;
                return ch_mount(c);
        }
        if (c->dirty) {
                c->dirty = 0;
                for (i = 0; i < 10000; i++) {
                        ch_pause10(c);
                        if (ch_ready(c))
                                break;
                        if (!ch_attached(c))
                                return 0;
                        ch_sense(c);
                }
        }
        return 1;
}

/* 0xAC2: reset the USB disk after a timeout and resync the data toggles */
static void ch_recover(struct ch375 *c)
{
        unsigned char v;

        ch_wait(c, 5);
        if (ch_attached(c) && ch_cmd_ok(c, CH_CMD_DISK_RESET)) {
                v = ch_read_var(c, CH_VAR_TOGGLE);
                ch_write_var(c, CH_VAR_TOGGLE, v & 0x3F);
                v = ch_read_var(c, CH_VAR_TOGGLE_IN);
                (void) ch_cmd1_ok(c, CH_CMD_SET_TOGGLE, ((v >> 4) & 0x0F) | 0x80);
                v = ch_read_var(c, CH_VAR_TOGGLE_OUT);
                (void) ch_cmd1_ok(c, CH_CMD_SET_TOGGLE, (v >> 4) & 0x0F);
        }
        ch_wait(c, 2);
}

/* 0x58C: after a failed transfer; returns 0 if the disk is gone */
static int ch_error(struct ch375 *c)
{
        if (!ch_attached(c) || !ch_mounted(c))
                return 0;
        ch_wait(c, 20);
        ch_sense(c);
        ch_wait(c, 2);
        c->retries++;
        return 1;
}

/*
 * 0x54D / 0x668: read or write count (1..255) sectors at lba.
 * Returns 1 on success.  The data loops use bare port I/O, as the DOS
 * driver does, since every packet is paced by the chip interrupt.
 */
static int ch_xfer(struct ch375 *c, int write, unsigned long lba,
                   unsigned int count, unsigned char *buf)
{
        unsigned int chunk, i;
        unsigned char *p;
        int st, try;

        if (!ch_prepare(c))
                return 0;
        if (write)
                c->dirty = 1;
        for (try = 0; try < 3; try++) {
                ch_cmd(c, write ? CH_CMD_DISK_WRITE : CH_CMD_DISK_READ);
                CH_OUTB(lba & 0xFF, c->base);
                CH_OUTB((lba >> 8) & 0xFF, c->base);
                CH_OUTB((lba >> 16) & 0xFF, c->base);
                CH_OUTB((lba >> 24) & 0xFF, c->base);
                CH_OUTB(count, c->base);
                p = buf;
                for (chunk = 0; chunk < count * (512 / CH_CHUNK); chunk++) {
                        st = ch_wait_int(c);
                        if (st != (write ? CH_INT_DISK_WRITE : CH_INT_DISK_READ))
                                goto fail;
                        if (write) {
                                CH_OUTB(CH_CMD_WR_USB_DATA7, c->base + 1);
                                CH_OUTB(CH_CHUNK, c->base);
                                for (i = 0; i < CH_CHUNK; i++)
                                        CH_OUTB(*p++, c->base);
                                CH_OUTB(CH_CMD_DISK_WR_GO, c->base + 1);
                        } else {
                                CH_OUTB(CH_CMD_RD_USB_DATA, c->base + 1);
                                (void) CH_INB(c->base);         /* length, always 64 */
                                for (i = 0; i < CH_CHUNK; i++)
                                        *p++ = CH_INB(c->base);
                                CH_OUTB(CH_CMD_DISK_RD_GO, c->base + 1);
                        }
                }
                st = ch_wait_int(c);
                if (st == CH_INT_SUCCESS)
                        return 1;
        fail:
                CH_LOG(("ch375: %s lba %lu count %u failed, status %d\n",
                        write ? "write" : "read", lba, count, st));
                if (st < 0)
                        ch_recover(c);
                if (!ch_error(c))
                        return 0;
        }
        return 0;
}

#endif /* CH375_HW_H */

