diff options
Diffstat (limited to 'drivers/tty/serial/sunhv.c')
| -rw-r--r-- | drivers/tty/serial/sunhv.c | 661 | 
1 files changed, 661 insertions, 0 deletions
diff --git a/drivers/tty/serial/sunhv.c b/drivers/tty/serial/sunhv.c new file mode 100644 index 00000000000..c9014868297 --- /dev/null +++ b/drivers/tty/serial/sunhv.c @@ -0,0 +1,661 @@ +/* sunhv.c: Serial driver for SUN4V hypervisor console. + * + * Copyright (C) 2006, 2007 David S. Miller (davem@davemloft.net) + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/tty.h> +#include <linux/tty_flip.h> +#include <linux/major.h> +#include <linux/circ_buf.h> +#include <linux/serial.h> +#include <linux/sysrq.h> +#include <linux/console.h> +#include <linux/spinlock.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/of_device.h> + +#include <asm/hypervisor.h> +#include <asm/spitfire.h> +#include <asm/prom.h> +#include <asm/irq.h> + +#if defined(CONFIG_MAGIC_SYSRQ) +#define SUPPORT_SYSRQ +#endif + +#include <linux/serial_core.h> + +#include "suncore.h" + +#define CON_BREAK	((long)-1) +#define CON_HUP		((long)-2) + +#define IGNORE_BREAK	0x1 +#define IGNORE_ALL	0x2 + +static char *con_write_page; +static char *con_read_page; + +static int hung_up = 0; + +static void transmit_chars_putchar(struct uart_port *port, struct circ_buf *xmit) +{ +	while (!uart_circ_empty(xmit)) { +		long status = sun4v_con_putchar(xmit->buf[xmit->tail]); + +		if (status != HV_EOK) +			break; + +		xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1); +		port->icount.tx++; +	} +} + +static void transmit_chars_write(struct uart_port *port, struct circ_buf *xmit) +{ +	while (!uart_circ_empty(xmit)) { +		unsigned long ra = __pa(xmit->buf + xmit->tail); +		unsigned long len, status, sent; + +		len = CIRC_CNT_TO_END(xmit->head, xmit->tail, +				      UART_XMIT_SIZE); +		status = sun4v_con_write(ra, len, &sent); +		if (status != HV_EOK) +			break; +		xmit->tail = (xmit->tail + sent) & (UART_XMIT_SIZE - 1); +		port->icount.tx += sent; +	} +} + +static int receive_chars_getchar(struct uart_port *port, struct tty_struct *tty) +{ +	int saw_console_brk = 0; +	int limit = 10000; + +	while (limit-- > 0) { +		long status; +		long c = sun4v_con_getchar(&status); + +		if (status == HV_EWOULDBLOCK) +			break; + +		if (c == CON_BREAK) { +			if (uart_handle_break(port)) +				continue; +			saw_console_brk = 1; +			c = 0; +		} + +		if (c == CON_HUP) { +			hung_up = 1; +			uart_handle_dcd_change(port, 0); +		} else if (hung_up) { +			hung_up = 0; +			uart_handle_dcd_change(port, 1); +		} + +		if (tty == NULL) { +			uart_handle_sysrq_char(port, c); +			continue; +		} + +		port->icount.rx++; + +		if (uart_handle_sysrq_char(port, c)) +			continue; + +		tty_insert_flip_char(tty, c, TTY_NORMAL); +	} + +	return saw_console_brk; +} + +static int receive_chars_read(struct uart_port *port, struct tty_struct *tty) +{ +	int saw_console_brk = 0; +	int limit = 10000; + +	while (limit-- > 0) { +		unsigned long ra = __pa(con_read_page); +		unsigned long bytes_read, i; +		long stat = sun4v_con_read(ra, PAGE_SIZE, &bytes_read); + +		if (stat != HV_EOK) { +			bytes_read = 0; + +			if (stat == CON_BREAK) { +				if (uart_handle_break(port)) +					continue; +				saw_console_brk = 1; +				*con_read_page = 0; +				bytes_read = 1; +			} else if (stat == CON_HUP) { +				hung_up = 1; +				uart_handle_dcd_change(port, 0); +				continue; +			} else { +				/* HV_EWOULDBLOCK, etc.  */ +				break; +			} +		} + +		if (hung_up) { +			hung_up = 0; +			uart_handle_dcd_change(port, 1); +		} + +		for (i = 0; i < bytes_read; i++) +			uart_handle_sysrq_char(port, con_read_page[i]); + +		if (tty == NULL) +			continue; + +		port->icount.rx += bytes_read; + +		tty_insert_flip_string(tty, con_read_page, bytes_read); +	} + +	return saw_console_brk; +} + +struct sunhv_ops { +	void (*transmit_chars)(struct uart_port *port, struct circ_buf *xmit); +	int (*receive_chars)(struct uart_port *port, struct tty_struct *tty); +}; + +static struct sunhv_ops bychar_ops = { +	.transmit_chars = transmit_chars_putchar, +	.receive_chars = receive_chars_getchar, +}; + +static struct sunhv_ops bywrite_ops = { +	.transmit_chars = transmit_chars_write, +	.receive_chars = receive_chars_read, +}; + +static struct sunhv_ops *sunhv_ops = &bychar_ops; + +static struct tty_struct *receive_chars(struct uart_port *port) +{ +	struct tty_struct *tty = NULL; + +	if (port->state != NULL)		/* Unopened serial console */ +		tty = port->state->port.tty; + +	if (sunhv_ops->receive_chars(port, tty)) +		sun_do_break(); + +	return tty; +} + +static void transmit_chars(struct uart_port *port) +{ +	struct circ_buf *xmit; + +	if (!port->state) +		return; + +	xmit = &port->state->xmit; +	if (uart_circ_empty(xmit) || uart_tx_stopped(port)) +		return; + +	sunhv_ops->transmit_chars(port, xmit); + +	if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS) +		uart_write_wakeup(port); +} + +static irqreturn_t sunhv_interrupt(int irq, void *dev_id) +{ +	struct uart_port *port = dev_id; +	struct tty_struct *tty; +	unsigned long flags; + +	spin_lock_irqsave(&port->lock, flags); +	tty = receive_chars(port); +	transmit_chars(port); +	spin_unlock_irqrestore(&port->lock, flags); + +	if (tty) +		tty_flip_buffer_push(tty); + +	return IRQ_HANDLED; +} + +/* port->lock is not held.  */ +static unsigned int sunhv_tx_empty(struct uart_port *port) +{ +	/* Transmitter is always empty for us.  If the circ buffer +	 * is non-empty or there is an x_char pending, our caller +	 * will do the right thing and ignore what we return here. +	 */ +	return TIOCSER_TEMT; +} + +/* port->lock held by caller.  */ +static void sunhv_set_mctrl(struct uart_port *port, unsigned int mctrl) +{ +	return; +} + +/* port->lock is held by caller and interrupts are disabled.  */ +static unsigned int sunhv_get_mctrl(struct uart_port *port) +{ +	return TIOCM_DSR | TIOCM_CAR | TIOCM_CTS; +} + +/* port->lock held by caller.  */ +static void sunhv_stop_tx(struct uart_port *port) +{ +	return; +} + +/* port->lock held by caller.  */ +static void sunhv_start_tx(struct uart_port *port) +{ +	transmit_chars(port); +} + +/* port->lock is not held.  */ +static void sunhv_send_xchar(struct uart_port *port, char ch) +{ +	unsigned long flags; +	int limit = 10000; + +	spin_lock_irqsave(&port->lock, flags); + +	while (limit-- > 0) { +		long status = sun4v_con_putchar(ch); +		if (status == HV_EOK) +			break; +		udelay(1); +	} + +	spin_unlock_irqrestore(&port->lock, flags); +} + +/* port->lock held by caller.  */ +static void sunhv_stop_rx(struct uart_port *port) +{ +} + +/* port->lock held by caller.  */ +static void sunhv_enable_ms(struct uart_port *port) +{ +} + +/* port->lock is not held.  */ +static void sunhv_break_ctl(struct uart_port *port, int break_state) +{ +	if (break_state) { +		unsigned long flags; +		int limit = 10000; + +		spin_lock_irqsave(&port->lock, flags); + +		while (limit-- > 0) { +			long status = sun4v_con_putchar(CON_BREAK); +			if (status == HV_EOK) +				break; +			udelay(1); +		} + +		spin_unlock_irqrestore(&port->lock, flags); +	} +} + +/* port->lock is not held.  */ +static int sunhv_startup(struct uart_port *port) +{ +	return 0; +} + +/* port->lock is not held.  */ +static void sunhv_shutdown(struct uart_port *port) +{ +} + +/* port->lock is not held.  */ +static void sunhv_set_termios(struct uart_port *port, struct ktermios *termios, +			      struct ktermios *old) +{ +	unsigned int baud = uart_get_baud_rate(port, termios, old, 0, 4000000); +	unsigned int quot = uart_get_divisor(port, baud); +	unsigned int iflag, cflag; +	unsigned long flags; + +	spin_lock_irqsave(&port->lock, flags); + +	iflag = termios->c_iflag; +	cflag = termios->c_cflag; + +	port->ignore_status_mask = 0; +	if (iflag & IGNBRK) +		port->ignore_status_mask |= IGNORE_BREAK; +	if ((cflag & CREAD) == 0) +		port->ignore_status_mask |= IGNORE_ALL; + +	/* XXX */ +	uart_update_timeout(port, cflag, +			    (port->uartclk / (16 * quot))); + +	spin_unlock_irqrestore(&port->lock, flags); +} + +static const char *sunhv_type(struct uart_port *port) +{ +	return "SUN4V HCONS"; +} + +static void sunhv_release_port(struct uart_port *port) +{ +} + +static int sunhv_request_port(struct uart_port *port) +{ +	return 0; +} + +static void sunhv_config_port(struct uart_port *port, int flags) +{ +} + +static int sunhv_verify_port(struct uart_port *port, struct serial_struct *ser) +{ +	return -EINVAL; +} + +static struct uart_ops sunhv_pops = { +	.tx_empty	= sunhv_tx_empty, +	.set_mctrl	= sunhv_set_mctrl, +	.get_mctrl	= sunhv_get_mctrl, +	.stop_tx	= sunhv_stop_tx, +	.start_tx	= sunhv_start_tx, +	.send_xchar	= sunhv_send_xchar, +	.stop_rx	= sunhv_stop_rx, +	.enable_ms	= sunhv_enable_ms, +	.break_ctl	= sunhv_break_ctl, +	.startup	= sunhv_startup, +	.shutdown	= sunhv_shutdown, +	.set_termios	= sunhv_set_termios, +	.type		= sunhv_type, +	.release_port	= sunhv_release_port, +	.request_port	= sunhv_request_port, +	.config_port	= sunhv_config_port, +	.verify_port	= sunhv_verify_port, +}; + +static struct uart_driver sunhv_reg = { +	.owner			= THIS_MODULE, +	.driver_name		= "sunhv", +	.dev_name		= "ttyS", +	.major			= TTY_MAJOR, +}; + +static struct uart_port *sunhv_port; + +/* Copy 's' into the con_write_page, decoding "\n" into + * "\r\n" along the way.  We have to return two lengths + * because the caller needs to know how much to advance + * 's' and also how many bytes to output via con_write_page. + */ +static int fill_con_write_page(const char *s, unsigned int n, +			       unsigned long *page_bytes) +{ +	const char *orig_s = s; +	char *p = con_write_page; +	int left = PAGE_SIZE; + +	while (n--) { +		if (*s == '\n') { +			if (left < 2) +				break; +			*p++ = '\r'; +			left--; +		} else if (left < 1) +			break; +		*p++ = *s++; +		left--; +	} +	*page_bytes = p - con_write_page; +	return s - orig_s; +} + +static void sunhv_console_write_paged(struct console *con, const char *s, unsigned n) +{ +	struct uart_port *port = sunhv_port; +	unsigned long flags; +	int locked = 1; + +	local_irq_save(flags); +	if (port->sysrq) { +		locked = 0; +	} else if (oops_in_progress) { +		locked = spin_trylock(&port->lock); +	} else +		spin_lock(&port->lock); + +	while (n > 0) { +		unsigned long ra = __pa(con_write_page); +		unsigned long page_bytes; +		unsigned int cpy = fill_con_write_page(s, n, +						       &page_bytes); + +		n -= cpy; +		s += cpy; +		while (page_bytes > 0) { +			unsigned long written; +			int limit = 1000000; + +			while (limit--) { +				unsigned long stat; + +				stat = sun4v_con_write(ra, page_bytes, +						       &written); +				if (stat == HV_EOK) +					break; +				udelay(1); +			} +			if (limit < 0) +				break; +			page_bytes -= written; +			ra += written; +		} +	} + +	if (locked) +		spin_unlock(&port->lock); +	local_irq_restore(flags); +} + +static inline void sunhv_console_putchar(struct uart_port *port, char c) +{ +	int limit = 1000000; + +	while (limit-- > 0) { +		long status = sun4v_con_putchar(c); +		if (status == HV_EOK) +			break; +		udelay(1); +	} +} + +static void sunhv_console_write_bychar(struct console *con, const char *s, unsigned n) +{ +	struct uart_port *port = sunhv_port; +	unsigned long flags; +	int i, locked = 1; + +	local_irq_save(flags); +	if (port->sysrq) { +		locked = 0; +	} else if (oops_in_progress) { +		locked = spin_trylock(&port->lock); +	} else +		spin_lock(&port->lock); + +	for (i = 0; i < n; i++) { +		if (*s == '\n') +			sunhv_console_putchar(port, '\r'); +		sunhv_console_putchar(port, *s++); +	} + +	if (locked) +		spin_unlock(&port->lock); +	local_irq_restore(flags); +} + +static struct console sunhv_console = { +	.name	=	"ttyHV", +	.write	=	sunhv_console_write_bychar, +	.device	=	uart_console_device, +	.flags	=	CON_PRINTBUFFER, +	.index	=	-1, +	.data	=	&sunhv_reg, +}; + +static int __devinit hv_probe(struct platform_device *op, const struct of_device_id *match) +{ +	struct uart_port *port; +	unsigned long minor; +	int err; + +	if (op->archdata.irqs[0] == 0xffffffff) +		return -ENODEV; + +	port = kzalloc(sizeof(struct uart_port), GFP_KERNEL); +	if (unlikely(!port)) +		return -ENOMEM; + +	minor = 1; +	if (sun4v_hvapi_register(HV_GRP_CORE, 1, &minor) == 0 && +	    minor >= 1) { +		err = -ENOMEM; +		con_write_page = kzalloc(PAGE_SIZE, GFP_KERNEL); +		if (!con_write_page) +			goto out_free_port; + +		con_read_page = kzalloc(PAGE_SIZE, GFP_KERNEL); +		if (!con_read_page) +			goto out_free_con_write_page; + +		sunhv_console.write = sunhv_console_write_paged; +		sunhv_ops = &bywrite_ops; +	} + +	sunhv_port = port; + +	port->line = 0; +	port->ops = &sunhv_pops; +	port->type = PORT_SUNHV; +	port->uartclk = ( 29491200 / 16 ); /* arbitrary */ + +	port->membase = (unsigned char __iomem *) __pa(port); + +	port->irq = op->archdata.irqs[0]; + +	port->dev = &op->dev; + +	err = sunserial_register_minors(&sunhv_reg, 1); +	if (err) +		goto out_free_con_read_page; + +	sunserial_console_match(&sunhv_console, op->dev.of_node, +				&sunhv_reg, port->line, false); + +	err = uart_add_one_port(&sunhv_reg, port); +	if (err) +		goto out_unregister_driver; + +	err = request_irq(port->irq, sunhv_interrupt, 0, "hvcons", port); +	if (err) +		goto out_remove_port; + +	dev_set_drvdata(&op->dev, port); + +	return 0; + +out_remove_port: +	uart_remove_one_port(&sunhv_reg, port); + +out_unregister_driver: +	sunserial_unregister_minors(&sunhv_reg, 1); + +out_free_con_read_page: +	kfree(con_read_page); + +out_free_con_write_page: +	kfree(con_write_page); + +out_free_port: +	kfree(port); +	sunhv_port = NULL; +	return err; +} + +static int __devexit hv_remove(struct platform_device *dev) +{ +	struct uart_port *port = dev_get_drvdata(&dev->dev); + +	free_irq(port->irq, port); + +	uart_remove_one_port(&sunhv_reg, port); + +	sunserial_unregister_minors(&sunhv_reg, 1); + +	kfree(port); +	sunhv_port = NULL; + +	dev_set_drvdata(&dev->dev, NULL); + +	return 0; +} + +static const struct of_device_id hv_match[] = { +	{ +		.name = "console", +		.compatible = "qcn", +	}, +	{ +		.name = "console", +		.compatible = "SUNW,sun4v-console", +	}, +	{}, +}; +MODULE_DEVICE_TABLE(of, hv_match); + +static struct of_platform_driver hv_driver = { +	.driver = { +		.name = "hv", +		.owner = THIS_MODULE, +		.of_match_table = hv_match, +	}, +	.probe		= hv_probe, +	.remove		= __devexit_p(hv_remove), +}; + +static int __init sunhv_init(void) +{ +	if (tlb_type != hypervisor) +		return -ENODEV; + +	return of_register_platform_driver(&hv_driver); +} + +static void __exit sunhv_exit(void) +{ +	of_unregister_platform_driver(&hv_driver); +} + +module_init(sunhv_init); +module_exit(sunhv_exit); + +MODULE_AUTHOR("David S. Miller"); +MODULE_DESCRIPTION("SUN4V Hypervisor console driver"); +MODULE_VERSION("2.0"); +MODULE_LICENSE("GPL");  |