/***********************************************************************
 *
 * (C) Copyright 2003-2004
 * DENX Software Engineering
 * Wolfgang Denk, wd@denx.de
 *
 * Simple PS2 serial driver
 *
 * Used by the PS/2 multiplexer driver (drivers/char/ps2mult.c)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 ***********************************************************************/

#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/serial.h>
#include <linux/serial_reg.h>
#include <linux/serialP.h>
#include <linux/ps2mult.h>
#include <asm/mpc5xxx.h>

#include <asm/system.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/atomic.h>
#ifdef CONFIG_UBOOT
#include <asm/ppcboot.h>
#endif

/* #define	DEBUG */

#define PS2SER_TTY	3
#define PS2SER_BAUD	57600

static int	ps2ser_getc_hw(void);
static void	ps2ser_interrupt(int irq, void *dev_id, struct pt_regs * regs);

extern struct	serial_state rs_table[]; /* psc.c */
static struct	serial_state *state = rs_table + PS2SER_TTY;
static struct	mpc5xxx_psc *psc;

static u_char	ps2buf[PS2BUF_SIZE];
static atomic_t	ps2buf_cnt;
static int	ps2buf_in_idx;
static int	ps2buf_out_idx;


int ps2ser_init (void)
{
	int divisor;
	int bits;
	int parity;
	int stop;
	int mode1;
	int mode2;
#ifdef CONFIG_UBOOT
	extern unsigned char __res[];
	bd_t *bd = (bd_t *)__res;
#endif
	psc = (struct mpc5xxx_psc *)state->iomem_base;

	/* Do not autoconfigure port on bootup
	 */
	state->flags &= ~ASYNC_BOOT_AUTOCONF;

#ifdef CONFIG_UBOOT
	/* round up */
	divisor = ((bd->bi_ipbfreq / (PS2SER_BAUD * 16)) + 1) >> 1;
	state->baud_base = bd->bi_ipbfreq;
#else
	/* round up */
	divisor = ((state->baud_base / (PS2SER_BAUD * 16)) + 1) >> 1;
#endif

	/* Configure the PSC for UART mode - not necessary if initialisattion
	 * is done in U-Boot.
	 */
#if 0
	val32 = in_be32(&gpio->port_config);
	val32 |= (MPC5xxx_GPIO_PSC_CONFIG_UART_WITHOUT_CD << (4*line));
	out_be32(&gpio->port_config, val32);
#endif
	/* reset and enable PSC */
	out_8(&psc->command, MPC5xxx_PSC_RST_TX
		  | MPC5xxx_PSC_RX_DISABLE | MPC5xxx_PSC_TX_ENABLE);
	out_8(&psc->command, MPC5xxx_PSC_RST_RX);
	/* Set PSC operation mode as 'UART, DCD ignored' */
	out_be32(&psc->sicr, 0x0);
	/* Set clocking */
	out_be16(&psc->mpc5xxx_psc_clock_select, 0xdd00);
	/* Set tx FIFO alarm level */
	out_be16(&psc->tfalarm, 0xf8);
	/* Put PSC into the operation */
	out_8(&psc->command, MPC5xxx_PSC_SEL_MODE_REG_1
		  | MPC5xxx_PSC_RX_ENABLE
		  | MPC5xxx_PSC_TX_ENABLE);

	out_8(&psc->ctur, divisor>>8);
	out_8(&psc->ctlr, divisor);

	/* Program hardware for parity, data bits, stop bits */
	stop = MPC5xxx_PSC_MODE_ONE_STOP;
	parity = MPC5xxx_PSC_MODE_PARNONE;
	bits = MPC5xxx_PSC_MODE_8_BITS;
	mode1 = bits | parity | MPC5xxx_PSC_MODE_ERR;
	mode2 = stop;
	out_8(&psc->command, MPC5xxx_PSC_SEL_MODE_REG_1);
	out_8(&psc->mode, mode1);
	out_8(&psc->mode, mode2);

	/* reqest irq and enable RX interrupt */
	if (request_irq (state->irq,
			    ps2ser_interrupt, SA_INTERRUPT, "ps2ser", NULL)) {
		printk ("ps2ser.c: request_irq() failed\n");
		return -1;
	}
	out_be16(&psc->mpc5xxx_psc_imr, MPC5xxx_PSC_IMR_RXRDY);

	return 0;
}

void ps2ser_putc (int chr)
{
#ifdef DEBUG
	printk (">>>> 0x%02x\n", chr);
#endif

	/*	while (!(ps2ser_in (UART_LSR) & UART_LSR_THRE)); */
	while (!(in_be16(&psc->mpc5xxx_psc_status)& MPC5xxx_PSC_SR_TXRDY));

	/*	ps2ser_out (UART_TX, chr); */
	out_8(&psc->mpc5xxx_psc_buffer_8, chr);
}

static int ps2ser_getc_hw (void)
{
	int res = -1;

	/*	if (ps2ser_in (UART_LSR) & UART_LSR_DR) {*/
	if (in_be16(&psc->mpc5xxx_psc_status) & MPC5xxx_PSC_SR_RXRDY) {
		res = (in_8(&psc->mpc5xxx_psc_buffer_8));
	}

	return res;
}

int ps2ser_getc (void)
{
	volatile int chr;
	unsigned long flags;

#ifdef DEBUG
	printk ("<< ");
#endif

	save_flags (flags);
	cli ();

	do {
		if (atomic_read (&ps2buf_cnt) != 0) {
			chr = ps2buf[ps2buf_out_idx++];
			ps2buf_out_idx &= (PS2BUF_SIZE - 1);
			atomic_dec (&ps2buf_cnt);
		} else {
			chr = ps2ser_getc_hw ();
		}
	}
	while (chr < 0);

	restore_flags (flags);

#ifdef DEBUG
	printk ("0x%02x\n", chr);
#endif

	return chr;
}

int ps2ser_check (void)
{
	unsigned long flags;

	save_flags (flags);
	cli ();
	ps2ser_interrupt (0, NULL, NULL);
	restore_flags (flags);

	return atomic_read (&ps2buf_cnt);
}

static void ps2ser_interrupt (int irq, void *dev_id, struct pt_regs *regs)
{
	int chr;
	unsigned int status;

	do {
		chr = ps2ser_getc_hw ();
		status = in_be16(&psc->mpc5xxx_psc_status);
		if (chr < 0)
			continue;

		if (atomic_read (&ps2buf_cnt) < PS2BUF_SIZE) {
			ps2buf[ps2buf_in_idx++] = chr;
			ps2buf_in_idx &= (PS2BUF_SIZE - 1);
			atomic_inc (&ps2buf_cnt);
		} else {
			printk ("ps2ser.c: buffer overflow\n");
		}
	}
	while ((status & MPC5xxx_PSC_SR_RXRDY));

	if (atomic_read (&ps2buf_cnt)) {
		ps2mult_callback (atomic_read (&ps2buf_cnt));
	}
}
