/*      $NetBSD: dz_ebus.c,v 1.10 2019/12/27 06:52:43 msaitoh Exp $     */

/*-
* Copyright (c) 2010 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code was written by Alessandro Forin and Neil Pittman
* at Microsoft Research and contributed to The NetBSD Foundation
* by Microsoft Corporation.
*
* 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: dz_ebus.c,v 1.10 2019/12/27 06:52:43 msaitoh Exp $");

#include "opt_ddb.h"

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/callout.h>
#include <sys/ioctl.h>
#include <sys/tty.h>
#include <sys/proc.h>
#include <sys/buf.h>
#include <sys/conf.h>
#include <sys/file.h>
#include <sys/uio.h>
#include <sys/kernel.h>
#include <sys/syslog.h>
#include <sys/device.h>
#include <sys/kauth.h>

#include <machine/bus.h>
#include <machine/emipsreg.h>

#include <dev/cons.h>


#include <emips/ebus/ebusvar.h>
#include <emips/emips/cons.h>
#if 0
#include <emips/emips/machdep.h>
#endif

#include "ioconf.h" /* for dz_cd */

#define DZ_C2I(c)       ((c) << 3)      /* convert controller # to index */
#define DZ_I2C(c)       ((c) >> 3)      /* convert minor to controller # */
#define DZ_PORT(u)      ((u) & 07)      /* extract the port # */

struct  dz_softc {
       device_t        sc_dev;         /* Autoconf blaha */
       struct  evcnt   sc_rintrcnt;    /* receive interrupt counts */
       struct  evcnt   sc_tintrcnt;    /* transmit interrupt counts */
       struct  _Usart  *sc_dr;         /* reg pointers */
       bus_space_tag_t sc_iot;
       bus_space_handle_t sc_ioh;
       int             sc_consline;    /* console line, or -1 */
       int             sc_rxint;       /* Receive interrupt count XXX */
       u_char          sc_brk;         /* Break asserted on some lines */
       u_char          sc_dsr;         /* DSR set bits if no mdm ctrl */
       struct dz_linestate {
               struct  dz_softc *dz_sc;        /* backpointer to softc */
               int             dz_line;        /* channel number */
               struct  tty     *dz_tty;        /* what we work on */
       } sc_dz;
};

void    dzrint(struct dz_softc *, uint32_t);
void    dzxint(struct dz_softc *, uint32_t);

#ifndef TIOCM_BRK
#define TIOCM_BRK               0100000         /* no equivalent */

static void     dzstart(struct tty *);
static int      dzparam(struct tty *, struct termios *);
static unsigned dzmctl(struct dz_softc *sc, int line,
                      int bits, /* one of the TIOCM_xx */
                      int how); /* one of the DMSET/BIS.. */

#include <dev/dec/dzkbdvar.h>
#endif

dev_type_open(dzopen);
dev_type_close(dzclose);
dev_type_read(dzread);
dev_type_write(dzwrite);
dev_type_ioctl(dzioctl);
dev_type_stop(dzstop);
dev_type_tty(dztty);
dev_type_poll(dzpoll);

const struct cdevsw dz_cdevsw = {
       .d_open = dzopen,
       .d_close = dzclose,
       .d_read = dzread,
       .d_write = dzwrite,
       .d_ioctl = dzioctl,
       .d_stop = dzstop,
       .d_tty = dztty,
       .d_poll = dzpoll,
       .d_mmap = nommap,
       .d_kqfilter = ttykqfilter,
       .d_discard = nodiscard,
       .d_flag = D_TTY
};

int
dzopen(dev_t dev, int flag, int mode, struct lwp *l)
{
       struct tty *tp;
       int unit, line;
       struct dz_softc *sc;
       int s, error = 0;

       unit = DZ_I2C(minor(dev));
       sc = device_lookup_private(&dz_cd, unit);
       if (sc == NULL)
               return ENXIO;

       line = DZ_PORT(minor(dev));
       if (line > 0) /* FIXME for more than one line */
               return ENXIO;

       tp = sc->sc_dz.dz_tty;
       if (tp == NULL)
               return ENODEV;
       tp->t_oproc = dzstart;
       tp->t_param = dzparam;
       tp->t_dev   = dev;

       if (kauth_authorize_device_tty(l->l_cred, KAUTH_DEVICE_TTY_OPEN, tp))
               return (EBUSY);

       if ((tp->t_state & TS_ISOPEN) == 0) {
               ttychars(tp);
               if (tp->t_ispeed == 0) {
                       tp->t_iflag = TTYDEF_IFLAG;
                       tp->t_oflag = TTYDEF_OFLAG;
                       tp->t_cflag = TTYDEF_CFLAG;
                       tp->t_lflag = TTYDEF_LFLAG;
                       tp->t_ispeed = tp->t_ospeed = TTYDEF_SPEED;
               }
               (void)dzparam(tp, &tp->t_termios);
               ttsetwater(tp);
       }
       /* we have no modem control but..*/
       if (dzmctl(sc, line, TIOCM_DTR, DMBIS) & TIOCM_CD)
               tp->t_state |= TS_CARR_ON;
       s = spltty();
       while (!(flag & O_NONBLOCK) && !(tp->t_cflag & CLOCAL) &&
           !(tp->t_state & TS_CARR_ON)) {
               tp->t_wopen++;
               error = ttysleep(tp, &tp->t_rawcv, true, 0);
               tp->t_wopen--;
               if (error)
                       break;
       }
       (void)splx(s);
       if (error)
               return error;
       return (*tp->t_linesw->l_open)(dev, tp);
}

int
dzclose(dev_t dev, int flag, int mode, struct lwp *l)
{
       struct dz_softc *sc;
       struct tty *tp;
       int unit, line;

       unit = DZ_I2C(minor(dev));
       sc = device_lookup_private(&dz_cd, unit);
       line = DZ_PORT(minor(dev));

       tp = sc->sc_dz.dz_tty;

       (*tp->t_linesw->l_close)(tp, flag);

       /* Make sure a BREAK state is not left enabled. */
       (void)dzmctl(sc, line, TIOCM_BRK, DMBIC);

       /* Do a hangup if so required. */
       if ((tp->t_cflag & HUPCL) || tp->t_wopen || !(tp->t_state & TS_ISOPEN))
               (void)dzmctl(sc, line, 0, DMSET);

       return ttyclose(tp);
}

int
dzread(dev_t dev, struct uio *uio, int flag)
{
       struct tty *tp;
       struct dz_softc *sc;

       sc = device_lookup_private(&dz_cd, DZ_I2C(minor(dev)));

       tp = sc->sc_dz.dz_tty;
       return (*tp->t_linesw->l_read)(tp, uio, flag);
}

int
dzwrite(dev_t dev, struct uio *uio, int flag)
{
       struct tty *tp;
       struct dz_softc *sc;

       sc = device_lookup_private(&dz_cd, DZ_I2C(minor(dev)));

       tp = sc->sc_dz.dz_tty;
       return (*tp->t_linesw->l_write)(tp, uio, flag);
}

/*ARGSUSED*/
int
dzioctl(dev_t dev, u_long cmd, void *data, int flag, struct lwp *l)
{
       struct  dz_softc *sc;
       struct tty *tp;
       int unit, line;
       int error;

       unit = DZ_I2C(minor(dev));
       line = 0;
       sc = device_lookup_private(&dz_cd, unit);
       tp = sc->sc_dz.dz_tty;

       error = (*tp->t_linesw->l_ioctl)(tp, cmd, data, flag, l);
       if (error >= 0)
               return error;

       error = ttioctl(tp, cmd, data, flag, l);
       if (error >= 0)
               return error;

       switch (cmd) {

       case TIOCSBRK:
               (void)dzmctl(sc, line, TIOCM_BRK, DMBIS);
               break;

       case TIOCCBRK:
               (void)dzmctl(sc, line, TIOCM_BRK, DMBIC);
               break;

       case TIOCSDTR:
               (void)dzmctl(sc, line, TIOCM_DTR, DMBIS);
               break;

       case TIOCCDTR:
               (void)dzmctl(sc, line, TIOCM_DTR, DMBIC);
               break;

       case TIOCMSET:
               (void)dzmctl(sc, line, *(int *)data, DMSET);
               break;

       case TIOCMBIS:
               (void)dzmctl(sc, line, *(int *)data, DMBIS);
               break;

       case TIOCMBIC:
               (void)dzmctl(sc, line, *(int *)data, DMBIC);
               break;

       case TIOCMGET:
               *(int *)data = dzmctl(sc, line, 0, DMGET) & ~TIOCM_BRK;
               break;

       default:
               return EPASSTHROUGH;
       }
       return 0;
}

/*ARGSUSED*/
void
dzstop(struct tty *tp, int flag)
{

       if (tp->t_state & TS_BUSY)
               if (!(tp->t_state & TS_TTSTOP))
                       tp->t_state |= TS_FLUSH;
}

struct tty *
dztty(dev_t dev)
{
       struct dz_softc *sc;
       struct tty *tp;

       sc = device_lookup_private(&dz_cd, DZ_I2C(minor(dev)));
       tp = sc->sc_dz.dz_tty;

       return tp;
}

int
dzpoll(dev_t dev, int events, struct lwp *l)
{
       struct dz_softc *sc;
       struct tty *tp;

       sc = device_lookup_private(&dz_cd, DZ_I2C(minor(dev)));

       tp = sc->sc_dz.dz_tty;
       return (*tp->t_linesw->l_poll)(tp, events, l);
}

void
dzstart(struct tty *tp)
{
       struct dz_softc *sc;
       struct clist *cl;
       int unit, s;

       unit = DZ_I2C(minor(tp->t_dev));
       sc = device_lookup_private(&dz_cd, unit);

       s = spltty();
       if (tp->t_state & (TS_TIMEOUT|TS_BUSY|TS_TTSTOP)) {
               splx(s);
               return;
       }
       cl = &tp->t_outq;
       ttypull(tp);
       if (cl->c_cc == 0) {
               splx(s);
               return;
       }

       tp->t_state |= TS_BUSY;

       /* was idle, get it started */
       dzxint(sc,USI_TXRDY);
       splx(s);
}

static int rclk = 25000000; /* BUGBUGBUGBUG */

static int
dzdivisor(int baudrate)
{
       int act_baud, divisor, error;

       if (baudrate <= 0)
               return 0;

       divisor = (rclk / 8) / (baudrate);
       divisor = (divisor / 2) + (divisor & 1);

       if (divisor <= 0)
               return -1;
       act_baud = rclk / (divisor * 16);

       /* 10 times error in percent: */
       error = ((act_baud - baudrate) * 2000 / baudrate + 1) >> 1;

       /* 3.0% maximum error tolerance: */
       if (error < -30 || error > 30)
               return -1;

       return divisor;
}

static int
dzparam(struct tty *tp, struct termios *t)
{
       struct  dz_softc *sc;
       int cflag = t->c_cflag;
       int unit, line;
       int speed;
       unsigned lpr;
       int s;
       struct _Usart *dzr;

       unit = DZ_I2C(minor(tp->t_dev));
       line = DZ_PORT(minor(tp->t_dev));
       sc = device_lookup_private(&dz_cd, unit);

       /* check requested parameters */
       if (t->c_ispeed != t->c_ospeed)
               return EINVAL;
       speed = dzdivisor(t->c_ispeed);
       if (speed < 0)
               return EINVAL;

       tp->t_ispeed = t->c_ispeed;
       tp->t_ospeed = t->c_ospeed;
       tp->t_cflag = cflag;

       {
               /* XXX */
               static int didit = 0;
               if (!didit && t->c_ispeed != 38400)
                       printf("dzparam: c_ispeed %d ignored, keeping 38400\n",
                           t->c_ispeed);
               didit = 1;
       }
       speed = dzdivisor(38400);

       if (speed == 0) {
               (void)dzmctl(sc, line, 0, DMSET);       /* hang up line */
               return 0;
       }

       switch (cflag & CSIZE) {
       case CS5:
               lpr = USC_BPC_5;
               break;
       case CS6:
               lpr = USC_BPC_6;
               break;
       case CS7:
               lpr = USC_BPC_7;
               break;
       default:
               lpr = USC_BPC_8;
               break;
       }
       if (cflag & CSTOPB)
               lpr |= USC_2STOP;

       if (cflag & PARENB) {
               if (cflag & PARODD)
                       lpr |= USC_ODD;
               else
                       lpr |= USC_EVEN;
       } else
               lpr |= USC_NONE;

       s = spltty();

       dzr = sc->sc_dr;

       dzr->Baud = speed;
       dzr->Control = USC_CLKDIV_4 | USC_TXEN | USC_RXEN | lpr;
#define USI_INTRS (USI_RXRDY|USI_RXBRK|USI_OVRE|USI_FRAME|USI_PARE)
       dzr->IntrEnable = USI_INTRS;

       (void)splx(s);
       return 0;
}

static unsigned
dzmctl(struct dz_softc *sc, int line, int bits, int how)
{
       unsigned int mbits;
       int s;
       struct _Usart *dzr;

       mbits = 0;

       s = spltty();

       dzr = sc->sc_dr;

       /* we have no modem control bits (CD,RI,DTR,DSR,..) */
       mbits |= TIOCM_CD;
       mbits |= TIOCM_DTR;

       if (dzr->ChannelStatus & USI_RXBRK)
               mbits |= TIOCM_BRK;

       switch (how) {
       case DMSET:
               mbits = bits;
               break;

       case DMBIS:
               mbits |= bits;
               break;

       case DMBIC:
               mbits &= ~bits;
               break;

       case DMGET:
               (void)splx(s);
               return mbits;
       }

       /* BUGBUG work in progress */
       if (mbits & TIOCM_BRK) {
               sc->sc_brk |= (1 << line);
               dzr->Control |= USC_STTBRK;
       } else {
               sc->sc_brk &= ~(1 << line);
               dzr->Control |= USC_STPBRK;
       }

       (void)splx(s);
       return mbits;
}


#if defined(DDB)
int dz_ddb = 0;
#endif

/* Receiver Interrupt */

void
dzrint(struct dz_softc *sc, uint32_t csr)
{
       struct tty *tp;
       int cc;
       struct _Usart *dzr;

       sc->sc_rxint++;
       dzr = sc->sc_dr;

       cc = dzr->RxData;
       tp = sc->sc_dz.dz_tty;

       /* clear errors before we print or bail out */
       if (csr & (USI_OVRE|USI_FRAME|USI_PARE))
               dzr->Control = USC_RSTSTA;

       if (!(tp->t_state & TS_ISOPEN)) {
               wakeup(&tp->t_rawq);
               return;
       }

       if (csr & USI_OVRE) {
               log(LOG_WARNING, "%s: silo overflow, line %d\n",
                   device_xname(sc->sc_dev), 0);
       }

       if (csr & USI_FRAME)
               cc |= TTY_FE;
       if (csr & USI_PARE)
               cc |= TTY_PE;

#if defined(DDB)
       /* ^P drops into DDB */
       if (dz_ddb && (cc == 0x10))
               Debugger();
#endif
       (*tp->t_linesw->l_rint)(cc, tp);
}

/* Transmitter Interrupt */

void
dzxint(struct dz_softc *sc, uint32_t csr)
{
       struct tty *tp;
       struct clist *cl;
       int ch;
       struct _Usart *dzr;

       dzr = sc->sc_dr;

       tp = sc->sc_dz.dz_tty;
       cl = &tp->t_outq;
       tp->t_state &= ~TS_BUSY;

       /* Just send out a char if we have one */
       if (cl->c_cc) {
               tp->t_state |= TS_BUSY;
               ch = getc(cl);
               dzr->TxData = ch;
               dzr->IntrEnable = USI_TXRDY;
               return;
       }

       /* Nothing to send; turn off intr */
       dzr->IntrDisable = USI_TXRDY;

       if (tp->t_state & TS_FLUSH)
               tp->t_state &= ~TS_FLUSH;
       else
               ndflush(&tp->t_outq, cl->c_cc);

       (*tp->t_linesw->l_start)(tp);
}

/*
* Machdep part of the driver
*/
int     dz_ebus_match(device_t, cfdata_t, void *);
void    dz_ebus_attach(device_t, device_t, void *);
int     dz_ebus_intr(void *, void *);

void    dz_ebus_cnsetup(paddr_t);
void    dz_ebus_cninit(struct consdev *);
int     dz_ebus_cngetc(dev_t);
void    dz_ebus_cnputc(dev_t, int);
void    dz_ebus_cnpollc(dev_t, int);

static int      dz_ebus_getmajor(void);

CFATTACH_DECL_NEW(dz_ebus, sizeof(struct dz_softc),
   dz_ebus_match, dz_ebus_attach, NULL, NULL);

struct consdev dz_ebus_consdev = {
       NULL, dz_ebus_cninit, dz_ebus_cngetc, dz_ebus_cnputc,
       dz_ebus_cnpollc, NULL, NULL, NULL, NODEV, CN_NORMAL,
};

/*
* Points to the console regs. Special mapping until VM is turned on.
*/
struct _Usart *dzcn;

int
dz_ebus_match(device_t parent, cfdata_t cf, void *aux)
{
       struct ebus_attach_args *iba;
       struct _Usart *us;

       iba = aux;

       if (strcmp(iba->ia_name, "dz") != 0)
               return 0;

       us = (struct _Usart *)iba->ia_vaddr;
       if ((us == NULL) ||
           (us->Tag != PMTTAG_USART))
               return 0;

       return 1;
}

void
dz_ebus_attach(device_t parent, device_t self, void *aux)
{
       struct ebus_attach_args *iba;
       struct dz_softc *sc;

       sc = device_private(self);
       iba = aux;

       sc->sc_dev = self;
       sc->sc_dr = (struct _Usart *)iba->ia_vaddr;
#if DEBUG
       printf(" virt=%p ", (void *)sc->sc_dr);
#endif

       printf(": neilsart 1 line");
       ebus_intr_establish(parent, (void *)iba->ia_cookie, IPL_TTY,
           dz_ebus_intr, sc);

       sc->sc_rxint = sc->sc_brk = 0;
       sc->sc_consline = 0;

       /* Initialize our softc structure. Should be done in open? */

       sc->sc_dz.dz_sc = sc;
       sc->sc_dz.dz_line = 0;
       sc->sc_dz.dz_tty = tty_alloc();

       evcnt_attach_dynamic(&sc->sc_rintrcnt, EVCNT_TYPE_INTR, NULL,
           device_xname(self), "rintr");
       evcnt_attach_dynamic(&sc->sc_tintrcnt, EVCNT_TYPE_INTR, NULL,
           device_xname(self), "tintr");

       /* Initialize hw regs */
#if 0
       DZ_WRITE_WORD(dr_csr, DZ_CSR_MSE | DZ_CSR_RXIE | DZ_CSR_TXIE);
       DZ_WRITE_BYTE(dr_dtr, 0);
       DZ_WRITE_BYTE(dr_break, 0);
#endif

       /* Switch the console to virtual mode */
       dzcn = sc->sc_dr;
       /* And test it */
       printf("\n");
}

static int
dz_ebus_getmajor(void)
{
       extern const struct cdevsw dz_cdevsw;
       static int cache = -1;

       if (cache != -1)
               return cache;

       return cache = cdevsw_lookup_major(&dz_cdevsw);
}

int
dz_ebus_intr(void *cookie, void *f)
{
       struct dz_softc *sc;
       struct _Usart *dzr;
       uint32_t csr;

       sc = cookie;
       dzr = sc->sc_dr;

#define USI_INTERRUPTS (USI_INTRS|USI_TXRDY)

       for (; ((csr = (dzr->ChannelStatus & dzr->IntrMask)) &
           USI_INTERRUPTS) != 0;) {
               if ((csr & USI_INTRS) != 0)
                       dzrint(sc, csr);
               if ((csr & USI_TXRDY) != 0)
                       dzxint(sc, csr);
       }

       return 0;
}

void
dz_ebus_cnsetup(paddr_t addr)
{

       dzcn = (struct _Usart *)addr;

#if 0
       /*
        * Initialize enough to xmit/recv via polling.
        * Bootloader might or might not have done it.
        */
       dzcn->Control =
           USC_RXEN |
           USC_TXEN |
           USC_BPC_8 |
           USC_NONE |
           USC_1STOP |
           USC_CLKDIV_4;
       dzcn->Baud = 0x29; /* 38400 */
#endif

       /*
        * Point the console at us
        */
       cn_tab = &dz_ebus_consdev;
       cn_tab->cn_pri = CN_NORMAL;/*CN_REMOTE?*/
       cn_tab->cn_dev = makedev(dz_ebus_getmajor(), 0);
}

void
dz_ebus_cninit(struct consdev *cn)
{
}

int
dz_ebus_cngetc(dev_t dev)
{
       int c, s;

       c = 0;
       s = spltty();

       while ((dzcn->ChannelStatus & USI_RXRDY) == 0)
               DELAY(10);
       c = dzcn->RxData;

       splx(s);
       if (c == 13) /* map cr->ln */
               c = 10;
       return c;
}

int dzflipped = 0;
void
dz_ebus_cnputc(dev_t dev, int ch)
{
       int timeout, s;

       /* Don't hang the machine! */
       timeout = 1 << 15;

       s = spltty();

#if 1
       /* Keep wired to hunt for a bug */
       if (dzcn && (dzcn != (struct _Usart *)0xfff90000)) {
               dzcn = (struct _Usart *)0xfff90000;
               dzflipped++;
       }
#endif

       /* Wait until ready */
       while ((dzcn->ChannelStatus & USI_TXRDY) == 0)
               if (--timeout < 0)
                       break;

       /* Put the character */
       dzcn->TxData = ch;

       splx(s);
}

/*
* Called before/after going into poll mode
*/
void
dz_ebus_cnpollc(dev_t dev, int on)
{
}