/*
 *	Serial driver for ADM5120 SoC
 *
 *	Derived from drivers/serial/uart00.c
 *	Copyright 2001 Altera Corporation
 *
 *	Some pieces are derived from the ADMtek 2.4 serial driver.
 *	Copyright (C) ADMtek Incorporated, 2003
 *		daniell@admtek.com.tw
 *	Which again was derived from drivers/char/serial.c
 *	Copyright (C) Linus Torvalds et al.
 *
 *	Copyright Jeroen Vreeken (pe1rxq@amsat.org), 2005
 */

#include <linux/autoconf.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/ioport.h>
#include <linux/serial.h>
#include <linux/serial_core.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
#include <linux/console.h>

#include <asm/mach-adm5120/adm5120_defs.h>
#include <asm/mach-adm5120/adm5120_irq.h>

#define ADM5120_UART_REG(base, reg) \
	(*(volatile u32 *)KSEG1ADDR((base)+(reg)))

#define ADM5120_UARTCLK_FREQ            62500000
#define ADM5120_UART_BAUDDIV(rate)	((unsigned long)(ADM5120_UARTCLK_FREQ/(16*(rate)) - 1))

#define ADM5120_UART_BAUD115200		ADM5120_UART_BAUDDIV(115200)

#define ADM5120_UART_DATA		0x00
#define ADM5120_UART_RS			0x04
#define ADM5120_UART_LCR_H		0x08
#define ADM5120_UART_LCR_M		0x0c
#define ADM5120_UART_LCR_L		0x10
#define ADM5120_UART_CR			0x14
#define ADM5120_UART_FR			0x18
#define ADM5120_UART_IR			0x1c

#define ADM5120_UART_FE			0x01
#define ADM5120_UART_PE			0x02
#define ADM5120_UART_BE			0x04
#define ADM5120_UART_OE			0x08
#define ADM5120_UART_ERR		0x0f
#define ADM5120_UART_FIFO_EN		0x10
#define ADM5120_UART_EN			0x01
#define ADM5120_UART_TIE		0x20
#define ADM5120_UART_RIE		0x50
#define ADM5120_UART_IE			0x78
#define ADM5120_UART_CTS		0x01
#define ADM5120_UART_DSR		0x02
#define ADM5120_UART_DCD		0x04
#define ADM5120_UART_TXFF		0x20
#define ADM5120_UART_TXFE		0x80
#define ADM5120_UART_RXFE		0x10
#define ADM5120_UART_BRK		0x01
#define ADM5120_UART_PEN		0x02
#define ADM5120_UART_EPS		0x04
#define ADM5120_UART_STP2		0x08
#define ADM5120_UART_W5			0x00
#define ADM5120_UART_W6			0x20
#define ADM5120_UART_W7			0x40
#define ADM5120_UART_W8			0x60
#define ADM5120_UART_MIS		0x01
#define ADM5120_UART_RIS		0x02
#define ADM5120_UART_TIS		0x04
#define ADM5120_UART_RTIS		0x08

static void adm5120ser_stop_tx(struct uart_port *port)
{
	ADM5120_UART_REG(port->iobase, ADM5120_UART_CR) &= ~ADM5120_UART_TIE;
}

static void adm5120ser_irq_rx(struct uart_port *port)
{
	struct tty_struct *tty = port->info->tty;
	unsigned int status, ch, rds, flg, ignored = 0;

	status = ADM5120_UART_REG(port->iobase, ADM5120_UART_FR);
	while (!(status & ADM5120_UART_RXFE)) {
		/* 
		 * We need to read rds before reading the 
		 * character from the fifo
		 */
		rds = ADM5120_UART_REG(port->iobase, ADM5120_UART_RS);
		ch = ADM5120_UART_REG(port->iobase, ADM5120_UART_DATA);
		port->icount.rx++;

		if (tty->low_latency)
			tty_flip_buffer_push(tty);

		flg = TTY_NORMAL;

		/*
		 * Note that the error handling code is
		 * out of the main execution path
		 */
		if (rds & ADM5120_UART_ERR)
			goto handle_error;
		if (uart_handle_sysrq_char(port, ch))
			goto ignore_char;

	error_return:
		tty_insert_flip_char(tty, ch, flg);

	ignore_char:
		status = ADM5120_UART_REG(port->iobase, ADM5120_UART_FR);
	}
 out:
	tty_flip_buffer_push(tty);
	return;

 handle_error:
 	ADM5120_UART_REG(port->iobase, ADM5120_UART_RS) = 0xff;
	if (rds & ADM5120_UART_BE) {
		port->icount.brk++;
		if (uart_handle_break(port))
			goto ignore_char;
	} else if (rds & ADM5120_UART_PE)
		port->icount.parity++;
	else if (rds & ADM5120_UART_FE)
		port->icount.frame++;
	if (rds & ADM5120_UART_OE)
		port->icount.overrun++;

	if (rds & port->ignore_status_mask) {
		if (++ignored > 100)
			goto out;
		goto ignore_char;
	}
	rds &= port->read_status_mask;

	if (rds & ADM5120_UART_BE)
		flg = TTY_BREAK;
	else if (rds & ADM5120_UART_PE)
		flg = TTY_PARITY;
	else if (rds & ADM5120_UART_FE)
		flg = TTY_FRAME;

	if (rds & ADM5120_UART_OE) {
		/*
		 * CHECK: does overrun affect the current character?
		 * ASSUMPTION: it does not.
		 */
		tty_insert_flip_char(tty, ch, flg);
		ch = 0;
		flg = TTY_OVERRUN;
	}
#ifdef CONFIG_MAGIC_SYSRQ
	port->sysrq = 0;
#endif
	goto error_return;
}

static void adm5120ser_irq_tx(struct uart_port *port)
{
	struct circ_buf *xmit = &port->info->xmit;
	int count;

	if (port->x_char) {
		ADM5120_UART_REG(port->iobase, ADM5120_UART_DATA) =
		    port->x_char;
		port->icount.tx++;
		port->x_char = 0;
		return;
	}
	if (uart_circ_empty(xmit) || uart_tx_stopped(port)) {
		adm5120ser_stop_tx(port);
		return;
	}

	count = port->fifosize >> 1;
	do {
		ADM5120_UART_REG(port->iobase, ADM5120_UART_DATA) =
		    xmit->buf[xmit->tail];
		xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
		port->icount.tx++;
		if (uart_circ_empty(xmit))
			break;
	} while (--count > 0);

	if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
		uart_write_wakeup(port);

	if (uart_circ_empty(xmit))
		adm5120ser_stop_tx(port);
}

static void adm5120ser_irq_modem(struct uart_port *port)
{
	unsigned int status;

	status = ADM5120_UART_REG(port->iobase, ADM5120_UART_FR);

	if (status & ADM5120_UART_DCD)
		uart_handle_dcd_change(port, status & ADM5120_UART_DCD);

	if (status & ADM5120_UART_DSR)
		port->icount.dsr++;

	if (status & ADM5120_UART_CTS)
		uart_handle_cts_change(port, status & ADM5120_UART_CTS);

	wake_up_interruptible(&port->info->delta_msr_wait);
}

static irqreturn_t adm5120ser_irq(int irq, void *dev_id)
{
	struct uart_port *port = dev_id;
	unsigned long ir = ADM5120_UART_REG(port->iobase, ADM5120_UART_IR);

	if (ir & (ADM5120_UART_RIS | ADM5120_UART_RTIS))
		adm5120ser_irq_rx(port);
	if (ir & ADM5120_UART_TIS)
		adm5120ser_irq_tx(port);
	if (ir & ADM5120_UART_MIS) {
		adm5120ser_irq_modem(port);
		ADM5120_UART_REG(port->iobase, ADM5120_UART_IR) = 0xff;
	}

	return IRQ_HANDLED;
}

static unsigned int adm5120ser_tx_empty(struct uart_port *port)
{
	unsigned int fr = ADM5120_UART_REG(port->iobase, ADM5120_UART_FR);
	return (fr & ADM5120_UART_TXFE) ? TIOCSER_TEMT : 0;
}

static void adm5120ser_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
}

static unsigned int adm5120ser_get_mctrl(struct uart_port *port)
{
	unsigned int result = 0;
	unsigned int fr = ADM5120_UART_REG(port->iobase, ADM5120_UART_FR);

	if (fr & ADM5120_UART_CTS)
		result |= TIOCM_CTS;
	if (fr & ADM5120_UART_DSR)
		result |= TIOCM_DSR;
	if (fr & ADM5120_UART_DCD)
		result |= TIOCM_CAR;
	return result;
}

static void adm5120ser_start_tx(struct uart_port *port)
{
	ADM5120_UART_REG(port->iobase, ADM5120_UART_CR) |= ADM5120_UART_TIE;
}

static void adm5120ser_stop_rx(struct uart_port *port)
{
	ADM5120_UART_REG(port->iobase, ADM5120_UART_CR) &= ~ADM5120_UART_RIE;
}

static void adm5120ser_enable_ms(struct uart_port *port)
{
}

static void adm5120ser_break_ctl(struct uart_port *port, int break_state)
{
	unsigned long flags;
	unsigned long lcrh;

	spin_lock_irqsave(&port->lock, flags);
	lcrh = ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_H);
	if (break_state == -1)
		lcrh |= ADM5120_UART_BRK;
	else
		lcrh &= ~ADM5120_UART_BRK;
	ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_H) = lcrh;
	spin_unlock_irqrestore(&port->lock, flags);
}

static int adm5120ser_startup(struct uart_port *port)
{
	int ret;

	ret = request_irq(port->irq, adm5120ser_irq, 0, "ADM5120 UART", port);
	if (ret) {
		printk(KERN_ERR "Couldn't get irq %d\n", port->irq);
		return ret;
	}
	ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_H) |=
	    ADM5120_UART_FIFO_EN;
	ADM5120_UART_REG(port->iobase, ADM5120_UART_CR) |=
	    ADM5120_UART_EN | ADM5120_UART_IE;
	return 0;
}

static void adm5120ser_shutdown(struct uart_port *port)
{
	ADM5120_UART_REG(port->iobase, ADM5120_UART_CR) &= ~ADM5120_UART_IE;
	free_irq(port->irq, port);
}

static void adm5120ser_set_termios(struct uart_port *port,
    struct ktermios *termios, struct ktermios *old)
{
	unsigned int baud, quot, lcrh;
	unsigned long flags;

	termios->c_cflag |= CREAD;

	baud = uart_get_baud_rate(port, termios, old, 0, port->uartclk/16);
	quot = uart_get_divisor(port, baud);

	lcrh = ADM5120_UART_FIFO_EN;
	switch (termios->c_cflag & CSIZE) {
		case CS5:
			lcrh |= ADM5120_UART_W5;
			break;
		case CS6:
			lcrh |= ADM5120_UART_W6;
			break;
		case CS7:
			lcrh |= ADM5120_UART_W7;
			break;
		default:
			lcrh |= ADM5120_UART_W8;
			break;
	}
	if (termios->c_cflag & CSTOPB)
		lcrh |= ADM5120_UART_STP2;
	if (termios->c_cflag & PARENB) {
		lcrh |= ADM5120_UART_PEN;
		if (!(termios->c_cflag & PARODD))
			lcrh |= ADM5120_UART_EPS;
	}

	spin_lock_irqsave(port->lock, flags);

	ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_H) = lcrh;

	/*
	 * Update the per-port timeout.
	 */
	uart_update_timeout(port, termios->c_cflag, baud);

	port->read_status_mask = ADM5120_UART_OE;
	if (termios->c_iflag & INPCK)
		port->read_status_mask |= ADM5120_UART_FE | ADM5120_UART_PE;
	if (termios->c_iflag & (BRKINT | PARMRK))
		port->read_status_mask |= ADM5120_UART_BE;

	/*
	 * Characters to ignore
	 */
	port->ignore_status_mask = 0;
	if (termios->c_iflag & IGNPAR)
		port->ignore_status_mask |= ADM5120_UART_FE | ADM5120_UART_PE;
	if (termios->c_iflag & IGNBRK) {
		port->ignore_status_mask |= ADM5120_UART_BE;
		/*
		 * If we're ignoring parity and break indicators,
		 * ignore overruns to (for real raw support).
		 */
		if (termios->c_iflag & IGNPAR)
			port->ignore_status_mask |= ADM5120_UART_OE;
	}

	quot = ADM5120_UART_BAUD115200;
	ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_L) = quot & 0xff;
	ADM5120_UART_REG(port->iobase, ADM5120_UART_LCR_M) = quot >> 8;

	spin_unlock_irqrestore(&port->lock, flags);
}

static const char *adm5120ser_type(struct uart_port *port)
{
	return port->type == PORT_ADM5120 ? "ADM5120" : NULL;
}

static void adm5120ser_config_port(struct uart_port *port, int flags)
{
	if (flags & UART_CONFIG_TYPE)
		port->type = PORT_ADM5120;
}

static void adm5120ser_release_port(struct uart_port *port)
{
	release_mem_region(port->iobase, ADM5120_UART_SIZE);
}

static int adm5120ser_request_port(struct uart_port *port)
{
	return request_mem_region(port->iobase, ADM5120_UART_SIZE,
	    "adm5120-uart") != NULL ? 0 : -EBUSY; 
}

static struct uart_ops adm5120ser_ops = {
	.tx_empty =	adm5120ser_tx_empty,
	.set_mctrl =	adm5120ser_set_mctrl,
	.get_mctrl =	adm5120ser_get_mctrl,
	.stop_tx =	adm5120ser_stop_tx,
	.start_tx =	adm5120ser_start_tx,
	.stop_rx =	adm5120ser_stop_rx,
	.enable_ms =	adm5120ser_enable_ms,
	.break_ctl =	adm5120ser_break_ctl,
	.startup =	adm5120ser_startup,
	.shutdown =	adm5120ser_shutdown,
	.set_termios =	adm5120ser_set_termios,
	.type =		adm5120ser_type,
	.config_port =	adm5120ser_config_port,
	.release_port =	adm5120ser_release_port,
	.request_port =	adm5120ser_request_port,
};

static void adm5120console_put(const char c)
{
	while ((ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_FR) &
	     ADM5120_UART_TXFF) != 0);
	ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_DATA) = c;
}

static void adm5120console_write(struct console *con, const char *s,
    unsigned int count)
{
	while (count--) {
		if (*s == '\n')
			adm5120console_put('\r');
		adm5120console_put(*s);
		s++;
	}
}

static int adm5120console_setup(struct console *con, char *options)
{
	/* Set to 115200 baud, 8N1 and enable FIFO */
	ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_LCR_L) =
	    ADM5120_UART_BAUD115200 & 0xff;
	ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_LCR_M) =
	    ADM5120_UART_BAUD115200 >> 8;
	ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_LCR_H) =
	    ADM5120_UART_W8 | ADM5120_UART_FIFO_EN;
	/* Enable port */
	ADM5120_UART_REG(ADM5120_UART0_BASE, ADM5120_UART_CR) =
	    ADM5120_UART_EN;

	return 0;
}

static struct uart_driver adm5120ser_reg;

static struct console adm5120_serconsole = {
	.name =		"ttyS",
	.write =	adm5120console_write,
	.device =	uart_console_device,
	.setup =	adm5120console_setup,
	.flags =	CON_PRINTBUFFER,
	.cflag =	B115200 | CS8 | CREAD,
	.index =	0,
	.data =		&adm5120ser_reg,
};

static int __init adm5120console_init(void)
{
	register_console(&adm5120_serconsole);
	return 0;
}

console_initcall(adm5120console_init);


static struct uart_port adm5120ser_ports[] = {
	{
		.iobase =	ADM5120_UART0_BASE,
		.irq =		ADM5120_IRQ_UART0,
		.uartclk =	ADM5120_UARTCLK_FREQ,
		.fifosize =	16,
		.ops =		&adm5120ser_ops,
		.line =		0,
		.flags =	ASYNC_BOOT_AUTOCONF,
	},
#if (CONFIG_ADM5120_NR_UARTS > 1)
	{
		.iobase =	ADM5120_UART1_BASE,
		.irq =		ADM5120_IRQ_UART1,
		.uartclk =	ADM5120_UARTCLK_FREQ,
		.fifosize =	16,
		.ops =		&adm5120ser_ops,
		.line =		1,
		.flags =	ASYNC_BOOT_AUTOCONF,
	},
#endif
};

static struct uart_driver adm5120ser_reg = {
	.owner	=	THIS_MODULE,
	.driver_name =	"ttyS",
	.dev_name =	"ttyS",
	.major =	TTY_MAJOR,
	.minor =	64,
	.nr =		CONFIG_ADM5120_NR_UARTS,
	.cons =		&adm5120_serconsole,
};

static int __init adm5120ser_init(void)
{
	int ret, i;

	ret = uart_register_driver(&adm5120ser_reg);
	if (!ret) {
		for (i = 0; i < CONFIG_ADM5120_NR_UARTS; i++)
			uart_add_one_port(&adm5120ser_reg, &adm5120ser_ports[i]);
	}

	return ret;
}

__initcall(adm5120ser_init);
