diff mbox series

[v2,2/4] tty/serial: ttvys: add null modem driver for emulation

Message ID 1581789069-5232-2-git-send-email-gupt21@gmail.com
State New
Headers show
Series None | expand

Commit Message

rishi gupta Feb. 15, 2020, 5:51 p.m. UTC
The ttyvs driver creates virtual tty devices that can be
used with standard POSIX APIs for serial port based applications.
The driver is used mainly for testing user space applications.

Devices can be created through device tree and through configfs.
Various serial port events are emulated through a sysfs file.

Signed-off-by: Rishi Gupta <gupt21@gmail.com>
---
Changes in v2:
- Used configFS instead of misc subsystem
- Added 'depends on CONFIGFS_FS' in Kconfig
- Used IDR instead of associative array
- Used tty_register_device_attr() to create sysfs with race with user-space
- Used u16 wherever applicable instead of ushort
- Use small x while defining hexadecimal numbers
- Merged faulty cable sysfs into event sysfs node itself
- Removed #define <linux/kernel.h>
- Added #define <linux/uaccess.h>
- Removed initial loopback & null-modem module parameters
- Removed variables for device accounting (not needed due to configfs)
- Used ATTRIBUTE_GROUPS macro

 MAINTAINERS          |    8 +
 drivers/tty/Kconfig  |   17 +
 drivers/tty/Makefile |    1 +
 drivers/tty/ttyvs.c  | 2010 ++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 2036 insertions(+)
 create mode 100644 drivers/tty/ttyvs.c
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index a0d8649..dedccfd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16973,6 +16973,14 @@  F:	include/uapi/linux/serial_core.h
 F:	include/uapi/linux/serial.h
 F:	include/uapi/linux/tty.h
 
+TTYVS VIRTUAL SERIAL DRIVER
+M:	Rishi Gupta <gupt21@gmail.com>
+L:	linux-serial@vger.kernel.org
+L:	linux-kernel@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/serial/ttyvs.yaml
+F:	drivers/tty/ttyvs.c
+
 TUA9001 MEDIA DRIVER
 M:	Antti Palosaari <crope@iki.fi>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/tty/Kconfig b/drivers/tty/Kconfig
index a312cb3..3acb6d6 100644
--- a/drivers/tty/Kconfig
+++ b/drivers/tty/Kconfig
@@ -477,4 +477,21 @@  config LDISC_AUTOLOAD
 	  dev.tty.ldisc_autoload sysctl, this configuration option will
 	  only set the default value of this functionality.
 
+config TTY_VS
+	tristate "Virtual serial null modem emulation"
+	depends on CONFIGFS_FS
+	help
+	  This driver creates virtual serial port devices (loopback and
+	  null modem style) that can be used in the same way as real serial
+	  port devices. Parity, frame, overflow, ring indicator, baudrate
+	  mismatch, hardware and software flow control can be emulated.
+
+	  For information about how to create/delete devices, exchange data
+	  and emulate events, please read:
+	  <file:Documentation/devicetree/bindings/serial/ttyvs.yaml>.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called ttyvs.ko. If you want to compile this driver
+	  into the kernel, say Y here.
+
 endif # TTY
diff --git a/drivers/tty/Makefile b/drivers/tty/Makefile
index 020b1cd..9d727ac 100644
--- a/drivers/tty/Makefile
+++ b/drivers/tty/Makefile
@@ -33,6 +33,7 @@  obj-$(CONFIG_SYNCLINK)		+= synclink.o
 obj-$(CONFIG_PPC_EPAPR_HV_BYTECHAN) += ehv_bytechan.o
 obj-$(CONFIG_GOLDFISH_TTY)	+= goldfish.o
 obj-$(CONFIG_MIPS_EJTAG_FDC_TTY) += mips_ejtag_fdc.o
+obj-$(CONFIG_TTY_VS)	+= ttyvs.o
 obj-$(CONFIG_VCC)		+= vcc.o
 
 obj-y += ipwireless/
diff --git a/drivers/tty/ttyvs.c b/drivers/tty/ttyvs.c
new file mode 100644
index 0000000..3104900
--- /dev/null
+++ b/drivers/tty/ttyvs.c
@@ -0,0 +1,2010 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Serial port null modem emulation driver
+ *
+ * Copyright (c) 2020, Rishi Gupta <gupt21@gmail.com>
+ */
+
+/*
+ * Virtual multi-port serial card:
+ *
+ * This driver implements a virtual multi-port serial card in such a
+ * way that the card can have 0 to N number of virtual serial ports
+ * (tty devices). These devices can be used using standard termios
+ * and Linux/Posix APIs.
+ *
+ * DT bindings: Documentation/devicetree/bindings/serial/ttyvs.yaml
+ * Usage: Documentation/virtual/tty-ttyvs.rst
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/errno.h>
+#include <linux/init.h>
+#include <linux/idr.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+#include <linux/tty.h>
+#include <linux/tty_driver.h>
+#include <linux/tty_flip.h>
+#include <linux/serial.h>
+#include <linux/sched.h>
+#include <linux/version.h>
+#include <linux/mutex.h>
+#include <linux/device.h>
+#include <linux/uaccess.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/configfs.h>
+
+/* Pin out configurations definitions */
+#define VS_CON_CTS    0x0001
+#define VS_CON_DCD    0x0002
+#define VS_CON_DSR    0x0004
+#define VS_CON_RI     0x0008
+
+/* Modem control register definitions */
+#define VS_MCR_DTR    0x0001
+#define VS_MCR_RTS    0x0002
+#define VS_MCR_LOOP   0x0004
+
+/* Modem status register definitions */
+#define VS_MSR_CTS    0x0008
+#define VS_MSR_DCD    0x0010
+#define VS_MSR_RI     0x0020
+#define VS_MSR_DSR    0x0040
+
+/* UART frame structure definitions */
+#define VS_CRTSCTS       0x0001
+#define VS_XON           0x0002
+#define VS_NONE          0x0004
+#define VS_DATA_5        0x0008
+#define VS_DATA_6        0x0010
+#define VS_DATA_7        0x0020
+#define VS_DATA_8        0x0040
+#define VS_PARITY_NONE   0x0080
+#define VS_PARITY_ODD    0x0100
+#define VS_PARITY_EVEN   0x0200
+#define VS_PARITY_MARK   0x0400
+#define VS_PARITY_SPACE  0x0800
+#define VS_STOP_1        0x1000
+#define VS_STOP_2        0x2000
+
+/* Constants for the device type (odevtyp) */
+#define VS_SNM 0x0001
+#define VS_CNM 0x0002
+#define VS_SLB 0x0003
+#define VS_CLB 0x0004
+
+/* Attributes associated with a configfs item (folder/device) */
+struct vs_cfs_dev_info {
+	struct config_group grp;
+	char *devtype;
+	int ownidx;
+	int peeridx;
+	u8 ortsmap;
+	u8 odtrmap;
+	u8 odtratopn;
+	u8 prtsmap;
+	u8 pdtrmap;
+	u8 pdtratopn;
+};
+
+/* Represents a virtual tty device in this virtual card */
+struct vs_dev {
+	/* index for this device in tty core */
+	unsigned int own_index;
+	/* index of the device to which this device is connected */
+	unsigned int peer_index;
+	/* shadow modem status register */
+	int msr_reg;
+	/* shadow modem control register */
+	int mcr_reg;
+	/* rts line connections for this device */
+	int rts_mappings;
+	/* dtr line connections for this device */
+	int dtr_mappings;
+	int set_odtr_at_open;
+	int set_pdtr_at_open;
+	int odevtyp;
+	/* mutual exclusion at device level */
+	struct mutex lock;
+	int is_break_on;
+	/* currently active baudrate */
+	int baud;
+	int uart_frame;
+	int waiting_msr_chg;
+	int tx_paused;
+	int faulty_cable;
+	struct tty_struct *own_tty;
+	struct tty_struct *peer_tty;
+	struct serial_struct serial;
+	struct async_icount icount;
+	struct device *device;
+};
+
+/*
+ * Index radix tree based database of all devices managed by
+ * this driver.
+ */
+static DEFINE_IDR(db);
+
+/* Used to create and destroy devices atomically/serially */
+static DEFINE_MUTEX(card_lock);
+
+/* Describes this driver */
+static struct tty_driver *ttyvs_driver;
+
+/* Maximum number of devices supported by this driver */
+static ushort max_num_vs_devs = 64;
+
+/*
+ * Notifies tty core that a framing/parity/overrun error has happend
+ * while receiving data on serial port. When frame or parity error
+ * happens, -7/-8 (randomly selected number by this driver) is sent as
+ * byte that got corrupted to tty core. For emulation purpose 0 can
+ * not be taken as corrupted byte because parity and break both will
+ * have same sequence (octal \377 \0 \0) and therefore application
+ * will not be able to differentiate between these two.
+ *
+ * This is also used for asserting/de-asserting ring event on line and
+ * notifies tty core when a break condition has been detected on line.
+ *
+ * 1. Emulate framing error:
+ * $ echo "1" > /sys/class/tty/ttyvsN/event
+ *
+ * 2. Emulate parity error:
+ * $ echo "2" > /sys/class/tty/ttyvsN/event
+ *
+ * 3. Emulate overrun error:
+ * $ echo "3" > /sys/class/tty/ttyvsN/event
+ *
+ * 4. Emulate ring indicator (set RI signal):
+ * $ echo "4" > /sys/class/tty/ttyvsN/event
+ *
+ * 5. Emulate ring indicator (unset RI signal):
+ * $ echo "5" > /sys/class/tty/ttyvsN/event
+ *
+ * 6. Emulate break received:
+ * $ echo "6" > /sys/class/tty/ttyvsN/event
+ *
+ * 7. Emulate cable is faulty (data sent but not received):
+ * $ echo "7" > /sys/class/tty/ttyvsN/event
+ *
+ * 8. Remove faulty cable condition:
+ * $ echo "8" > /sys/class/tty/ttyvsN/event
+ */
+static ssize_t event_store(struct device *dev,
+		struct device_attribute *attr, const char *buf, size_t count)
+{
+	int ret, push = 1;
+	struct vs_dev *local_vsdev = dev_get_drvdata(dev);
+	struct tty_struct *tty_to_write = local_vsdev->own_tty;
+
+	if (!buf || (count <= 0))
+		return -EINVAL;
+
+	/*
+	 * Ensure required structure has been allocated, initialized and
+	 * port has been opened.
+	 */
+	if (!tty_to_write || (tty_to_write->port == NULL)
+			|| (tty_to_write->port->count <= 0))
+		return -EIO;
+	if (!test_bit(ASYNCB_INITIALIZED, &tty_to_write->port->flags))
+		return -EIO;
+
+	mutex_lock(&local_vsdev->lock);
+
+	switch (buf[0]) {
+	case '1':
+		ret = tty_insert_flip_char(tty_to_write->port, -7, TTY_FRAME);
+		if (ret < 0)
+			goto fail;
+		local_vsdev->icount.frame++;
+		break;
+	case '2':
+		ret = tty_insert_flip_char(tty_to_write->port, -8, TTY_PARITY);
+		if (ret < 0)
+			goto fail;
+		local_vsdev->icount.parity++;
+		break;
+	case '3':
+		ret = tty_insert_flip_char(tty_to_write->port, 0, TTY_OVERRUN);
+		if (ret < 0)
+			goto fail;
+		local_vsdev->icount.overrun++;
+		break;
+	case '4':
+		local_vsdev->msr_reg |= VS_MSR_RI;
+		local_vsdev->icount.rng++;
+		push = -1;
+		break;
+	case '5':
+		local_vsdev->msr_reg &= ~VS_MSR_RI;
+		local_vsdev->icount.rng++;
+		push = -1;
+		break;
+	case '6':
+		ret = tty_insert_flip_char(tty_to_write->port, 0, TTY_BREAK);
+		if (ret < 0)
+			goto fail;
+		local_vsdev->icount.brk++;
+		break;
+	case '7':
+		local_vsdev->faulty_cable = 1;
+		push = -1;
+		break;
+	case '8':
+		local_vsdev->faulty_cable = 0;
+		push = -1;
+		break;
+	default:
+		mutex_unlock(&local_vsdev->lock);
+		return -EINVAL;
+	}
+
+	if (push)
+		tty_flip_buffer_push(tty_to_write->port);
+	ret = count;
+
+fail:
+	mutex_unlock(&local_vsdev->lock);
+	return ret;
+}
+static DEVICE_ATTR_WO(event);
+
+static struct attribute *ttyvs_attrs[] = {
+	&dev_attr_event.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(ttyvs);
+
+/*
+ * Checks if the given serial port has received its carrier detect
+ * line raised or not. Return 1 if the carrier is raised otherwise 0.
+ */
+static int vs_port_carrier_raised(struct tty_port *port)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, port->tty->index);
+
+	return (local_vsdev->msr_reg & VS_MSR_DCD) ? 1 : 0;
+}
+
+/* Shutdown the given serial port */
+static void vs_port_shutdown(struct tty_port *port)
+{
+	pr_debug("shutting down the port!\n");
+}
+
+/*
+ * Invoked when tty is going to be destroyed and driver should
+ * release resources.
+ */
+static void vs_port_destruct(struct tty_port *port)
+{
+	pr_debug("destroying the port!\n");
+}
+
+/* Activate the given serial port as opposed to shutdown */
+static int vs_port_activate(struct tty_port *port, struct tty_struct *tty)
+{
+	return 0;
+}
+
+static const struct tty_port_operations vs_port_ops = {
+	.carrier_raised = vs_port_carrier_raised,
+	.shutdown       = vs_port_shutdown,
+	.activate       = vs_port_activate,
+	.destruct       = vs_port_destruct,
+};
+
+/*
+ * Update modem control and status registers according to the bit
+ * mask(s) provided. The RTS and DTR values can be set only if the
+ * current handshaking state of the tty device allows direct control
+ * of the modem control lines. The pin mappings are honoured.
+ *
+ * Caller holds lock of thegiven virtual tty device.
+ */
+static int vs_update_modem_lines(struct tty_struct *tty,
+			unsigned int set, unsigned int clear)
+{
+	int ctsint = 0;
+	int dcdint = 0;
+	int dsrint = 0;
+	int rngint = 0;
+	int mcr_ctrl_reg = 0;
+	int wakeup_blocked_open = 0;
+	int rts_mappings, dtr_mappings, msr_state_reg;
+	struct async_icount *evicount;
+	struct vs_dev *vsdev, *local_vsdev, *remote_vsdev;
+
+	local_vsdev = idr_find(&db, tty->index);
+
+	/* Read modify write MSR register */
+	if (tty->index != local_vsdev->peer_index) {
+		remote_vsdev = idr_find(&db, local_vsdev->peer_index);
+		msr_state_reg = remote_vsdev->msr_reg;
+		vsdev = remote_vsdev;
+	} else {
+		msr_state_reg = local_vsdev->msr_reg;
+		vsdev = local_vsdev;
+	}
+
+	rts_mappings = local_vsdev->rts_mappings;
+	dtr_mappings = local_vsdev->dtr_mappings;
+
+	if (set & TIOCM_RTS) {
+		mcr_ctrl_reg |= VS_MCR_RTS;
+		if ((rts_mappings & VS_CON_CTS) == VS_CON_CTS) {
+			msr_state_reg |= VS_MSR_CTS;
+			ctsint++;
+		}
+		if ((rts_mappings & VS_CON_DCD) == VS_CON_DCD) {
+			msr_state_reg |= VS_MSR_DCD;
+			dcdint++;
+			wakeup_blocked_open = 1;
+		}
+		if ((rts_mappings & VS_CON_DSR) == VS_CON_DSR) {
+			msr_state_reg |= VS_MSR_DSR;
+			dsrint++;
+		}
+		if ((rts_mappings & VS_CON_RI) == VS_CON_RI) {
+			msr_state_reg |= VS_MSR_RI;
+			rngint++;
+		}
+	}
+
+	if (set & TIOCM_DTR) {
+		mcr_ctrl_reg |= VS_MCR_DTR;
+		if ((dtr_mappings & VS_CON_CTS) == VS_CON_CTS) {
+			msr_state_reg |= VS_MSR_CTS;
+			ctsint++;
+		}
+		if ((dtr_mappings & VS_CON_DCD) == VS_CON_DCD) {
+			msr_state_reg |= VS_MSR_DCD;
+			dcdint++;
+			wakeup_blocked_open = 1;
+		}
+		if ((dtr_mappings & VS_CON_DSR) == VS_CON_DSR) {
+			msr_state_reg |= VS_MSR_DSR;
+			dsrint++;
+		}
+		if ((dtr_mappings & VS_CON_RI) == VS_CON_RI) {
+			msr_state_reg |= VS_MSR_RI;
+			rngint++;
+		}
+	}
+
+	if (clear & TIOCM_RTS) {
+		mcr_ctrl_reg &= ~VS_MCR_RTS;
+		if ((rts_mappings & VS_CON_CTS) == VS_CON_CTS) {
+			msr_state_reg &= ~VS_MSR_CTS;
+			ctsint++;
+		}
+		if ((rts_mappings & VS_CON_DCD) == VS_CON_DCD) {
+			msr_state_reg &= ~VS_MSR_DCD;
+			dcdint++;
+		}
+		if ((rts_mappings & VS_CON_DSR) == VS_CON_DSR) {
+			msr_state_reg &= ~VS_MSR_DSR;
+			dsrint++;
+		}
+		if ((rts_mappings & VS_CON_RI) == VS_CON_RI) {
+			msr_state_reg &= ~VS_MSR_RI;
+			rngint++;
+		}
+	}
+
+	if (clear & TIOCM_DTR) {
+		mcr_ctrl_reg &= ~VS_MCR_DTR;
+		if ((dtr_mappings & VS_CON_CTS) == VS_CON_CTS) {
+			msr_state_reg &= ~VS_MSR_CTS;
+			ctsint++;
+		}
+		if ((dtr_mappings & VS_CON_DCD) == VS_CON_DCD) {
+			msr_state_reg &= ~VS_MSR_DCD;
+			dcdint++;
+		}
+		if ((dtr_mappings & VS_CON_DSR) == VS_CON_DSR) {
+			msr_state_reg &= ~VS_MSR_DSR;
+			dsrint++;
+		}
+		if ((dtr_mappings & VS_CON_RI) == VS_CON_RI) {
+			msr_state_reg &= ~VS_MSR_RI;
+			rngint++;
+		}
+	}
+
+	local_vsdev->mcr_reg = mcr_ctrl_reg;
+	vsdev->msr_reg = msr_state_reg;
+
+	evicount = &vsdev->icount;
+	evicount->cts += ctsint;
+	evicount->dsr += dsrint;
+	evicount->dcd += dcdint;
+	evicount->rng += rngint;
+
+	if (vsdev->own_tty && vsdev->own_tty->port) {
+		/* Wake up process blocked on TIOCMIWAIT ioctl */
+		if ((vsdev->waiting_msr_chg == 1) &&
+				(vsdev->own_tty->port->count > 0)) {
+			wake_up_interruptible(
+					&vsdev->own_tty->port->delta_msr_wait);
+		}
+
+		/* Wake up application blocked on carrier detect signal */
+		if ((wakeup_blocked_open == 1) &&
+				(vsdev->own_tty->port->blocked_open > 0)) {
+			wake_up_interruptible(&vsdev->own_tty->port->open_wait);
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Invoked when user space process opens a serial port. The tty core
+ * calls this to install tty and initialize the required resources.
+ */
+static int vs_install(struct tty_driver *drv, struct tty_struct *tty)
+{
+	int ret;
+	struct tty_port *port;
+
+	port = kcalloc(1, sizeof(struct tty_port), GFP_KERNEL);
+	if (!port)
+		return -ENOMEM;
+
+	/* First initialize and then set port operations */
+	tty_port_init(port);
+	port->ops = &vs_port_ops;
+
+	ret = tty_port_install(port, drv, tty);
+	if (ret) {
+		kfree(port);
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * Invoked when there exist no user process or tty is to be
+ * released explicitly for whatever reason.
+ */
+static void vs_cleanup(struct tty_struct *tty)
+{
+	tty_port_put(tty->port);
+}
+
+/*
+ * Called when open system call is called on virtual tty device node.
+ * The tty core allocates 'struct tty_struct' for this device and
+ * set up various resources, sets up line discipline and call this
+ * function. For first time allocation happens and from next time
+ * onwards only re-opening happens.
+ *
+ * The tty core finds the tty driver serving this device node and the
+ * index of this tty device as registered by this driver with tty core.
+ * From this inded we retrieve the virtual tty device to work on.
+ *
+ * If the same serial port is opened more than once, the tty structure
+ * passed to this function will be same but filp structure will be
+ * different every time. Caller holds tty lock.
+ *
+ * This driver does not set CLOCAL by default. This means that the
+ * open() system call will block until it find its carrier detect
+ * line raised. Application should use O_NONBLOCK/O_NDELAY flag if
+ * it does not want to wait for DCD line change.
+ */
+static int vs_open(struct tty_struct *tty, struct file *filp)
+{
+	int ret;
+	struct vs_dev *remote_vsdev;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	local_vsdev->own_tty = tty;
+
+	/*
+	 * If this device is one end of a null modem connection,
+	 * provide its address to remote end.
+	 */
+	if (tty->index != local_vsdev->peer_index) {
+		remote_vsdev = idr_find(&db, local_vsdev->peer_index);
+		remote_vsdev->peer_tty = tty;
+	}
+
+	memset(&local_vsdev->serial, 0, sizeof(struct serial_struct));
+	memset(&local_vsdev->icount, 0, sizeof(struct async_icount));
+
+	/*
+	 * Handle DTR raising logic ourselve instead of tty_port helpers
+	 * doing it. Locking virtual tty is not required here.
+	 */
+	if (local_vsdev->set_odtr_at_open == 1)
+		vs_update_modem_lines(tty, TIOCM_DTR | TIOCM_RTS, 0);
+
+	/* Associate tty with port and do port level opening. */
+	ret = tty_port_open(tty->port, tty, filp);
+	if (ret)
+		return ret;
+
+	tty->port->close_delay  = 0;
+	tty->port->closing_wait = ASYNC_CLOSING_WAIT_NONE;
+	tty->port->drain_delay  = 0;
+
+	return ret;
+}
+
+/*
+ * Invoked by tty layer when release() is called on the file pointer
+ * that was previously created with a call to open().
+ */
+static void vs_close(struct tty_struct *tty, struct file *filp)
+{
+	if (test_bit(TTY_IO_ERROR, &tty->flags))
+		return;
+
+	if (tty && filp && tty->port && (tty->port->count > 0))
+		tty_port_close(tty->port, tty, filp);
+
+	if (tty && C_HUPCL(tty) && tty->port && (tty->port->count < 1))
+		vs_update_modem_lines(tty, 0, TIOCM_DTR | TIOCM_RTS);
+}
+
+/*
+ * Invoked when write() system call is invoked on device node.
+ * This function constructs evry byte as per the current uart
+ * frame settings. Finally, the data is inserted into the tty
+ * buffer of the receiver tty device.
+ */
+static int vs_write(struct tty_struct *tty,
+			const unsigned char *buf, int count)
+{
+	int x;
+	unsigned char *data = NULL;
+	struct tty_struct *tty_to_write = NULL;
+	struct vs_dev *rx_vsdev = NULL;
+	struct vs_dev *tx_vsdev = idr_find(&db, tty->index);
+
+	if (tx_vsdev->tx_paused || !tty || tty->stopped
+			|| (count < 1) || !buf || tty->hw_stopped)
+		return 0;
+
+	if (tx_vsdev->is_break_on == 1) {
+		pr_debug("break condition is on!\n");
+		return -EIO;
+	}
+
+	if (tx_vsdev->faulty_cable == 1)
+		return count;
+
+	if (tty->index != tx_vsdev->peer_index) {
+		/* Null modem */
+		tty_to_write = tx_vsdev->peer_tty;
+		rx_vsdev = idr_find(&db, tx_vsdev->peer_index);
+
+		if ((tx_vsdev->baud != rx_vsdev->baud) ||
+			(tx_vsdev->uart_frame != rx_vsdev->uart_frame)) {
+			/*
+			 * Emulate data sent but not received due to
+			 * mismatched baudrate/framing.
+			 */
+			pr_debug("mismatched serial port settings!\n");
+			tx_vsdev->icount.tx++;
+			return count;
+		}
+	} else {
+		/* Loop back */
+		tty_to_write = tty;
+		rx_vsdev = tx_vsdev;
+	}
+
+	if (tty_to_write) {
+		if ((tty_to_write->termios.c_cflag & CSIZE) == CS8) {
+			data = (unsigned char *)buf;
+		} else {
+			data = kcalloc(count, sizeof(char), GFP_KERNEL);
+			if (!data)
+				return -ENOMEM;
+
+			/* Emulate correct number of data bits */
+			switch (tty_to_write->termios.c_cflag & CSIZE) {
+			case CS7:
+				for (x = 0; x < count; x++)
+					data[x] = buf[x] & 0x7F;
+				break;
+			case CS6:
+				for (x = 0; x < count; x++)
+					data[x] = buf[x] & 0x3F;
+				break;
+			case CS5:
+				for (x = 0; x < count; x++)
+					data[x] = buf[x] & 0x1F;
+				break;
+			default:
+				data = (unsigned char *)buf;
+			}
+		}
+
+		tty_insert_flip_string(tty_to_write->port, data, count);
+		tty_flip_buffer_push(tty_to_write->port);
+		tx_vsdev->icount.tx++;
+		rx_vsdev->icount.rx++;
+
+		if (data != buf)
+			kfree(data);
+	} else {
+		/*
+		 * Other end is still not opened, emulate transmission from
+		 * local end but don't make other end receive it as is the
+		 * case in real world.
+		 */
+		tx_vsdev->icount.tx++;
+	}
+
+	return count;
+}
+
+/* Invoked by tty core to transmit single data byte. */
+static int vs_put_char(struct tty_struct *tty, unsigned char ch)
+{
+	unsigned char data;
+	struct tty_struct *tty_to_write;
+	struct vs_dev *rx_vsdev;
+	struct vs_dev *tx_vsdev = idr_find(&db, tty->index);
+
+	if (tx_vsdev->tx_paused || !tty || tty->stopped || tty->hw_stopped)
+		return 0;
+
+	if (tx_vsdev->is_break_on == 1)
+		return -EIO;
+
+	if (tx_vsdev->faulty_cable == 1)
+		return 1;
+
+	if (tty->index != tx_vsdev->peer_index) {
+		tty_to_write = tx_vsdev->peer_tty;
+		rx_vsdev = idr_find(&db, tx_vsdev->peer_index);
+		if ((tx_vsdev->baud != rx_vsdev->baud) ||
+			(tx_vsdev->uart_frame != rx_vsdev->uart_frame)) {
+			tx_vsdev->icount.tx++;
+			return 1;
+		}
+	} else {
+		tty_to_write = tty;
+		rx_vsdev = tx_vsdev;
+	}
+
+	if (tty_to_write != NULL) {
+		switch (tty_to_write->termios.c_cflag & CSIZE) {
+		case CS8:
+			data = ch;
+			break;
+		case CS7:
+			data = ch & 0x7F;
+			break;
+		case CS6:
+			data = ch & 0x3F;
+			break;
+		case CS5:
+			data = ch & 0x1F;
+			break;
+		default:
+			data = ch;
+		}
+		tty_insert_flip_string(tty_to_write->port, &data, 1);
+		tty_flip_buffer_push(tty_to_write->port);
+		tx_vsdev->icount.tx++;
+		rx_vsdev->icount.rx++;
+	} else {
+		tx_vsdev->icount.tx++;
+	}
+
+	return 1;
+}
+
+/*
+ * Flush the data out of serial port. This driver immediately
+ * pushes data into receiver's tty buffer hence do nothing here.
+ */
+static void vs_flush_chars(struct tty_struct *tty)
+{
+	pr_debug("flushing the chars!\n");
+}
+
+/*
+ * Discard the internal output buffer for this tty device. Typically
+ * it may be called when executing IOCTL TCOFLUSH, closing the
+ * serial port, when break is received in input stream (flushing
+ * is configured) or when hangup occurs.
+ *
+ * On the other hand, when TCIFLUSH IOCTL is invoked, tty flip buffer
+ * and line discipline queue gets emptied without involvement of tty
+ * driver. The driver is generally expected not to keep data but send
+ * it to tty layer as soon as possible when it receives data.
+ *
+ * As this driver immediately pushes data into receiver's tty buffer
+ * hence do nothing here.
+ */
+static void vs_flush_buffer(struct tty_struct *tty)
+{
+	pr_debug("flushing the buffer!\n");
+}
+
+/* Provides information as a repsonse to TIOCGSERIAL IOCTL */
+static int vs_get_serinfo(struct tty_struct *tty, unsigned long arg)
+{
+	int ret;
+	struct serial_struct info;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+	struct serial_struct serial = local_vsdev->serial;
+
+	if (!arg)
+		return -EFAULT;
+
+	memset(&info, 0, sizeof(info));
+
+	info.type		    = PORT_UNKNOWN;
+	info.line		    = serial.line;
+	info.port		    = tty->index;
+	info.irq			= 0;
+	info.flags		    = tty->port->flags;
+	info.xmit_fifo_size = 0;
+	info.baud_base	    = 0;
+	info.close_delay	= tty->port->close_delay;
+	info.closing_wait   = tty->port->closing_wait;
+	info.custom_divisor = 0;
+	info.hub6		    = 0;
+	info.io_type		= SERIAL_IO_MEM;
+
+	ret = copy_to_user((void __user *)arg, &info,
+				sizeof(struct serial_struct));
+
+	return ret ? -EFAULT : 0;
+}
+
+/* Returns number of bytes that can be queued to this device now */
+static int vs_write_room(struct tty_struct *tty)
+{
+	struct vs_dev *tx_vsdev = idr_find(&db, tty->index);
+
+	if (tx_vsdev->tx_paused || !tty ||
+			tty->stopped || tty->hw_stopped)
+		return 0;
+
+	return 2048;
+}
+
+/*
+ * Invoked when serial terminal settings are chaged. The old_termios
+ * contains currently active settings and tty->termios contains new
+ * settings to be applied.
+ */
+static void vs_set_termios(struct tty_struct *tty,
+				struct ktermios *old_termios)
+{
+	u32 baud;
+	int uart_frame_settings;
+	unsigned int mask = TIOCM_DTR;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+
+	/*
+	 * Typically B0 is used to terminate the connection.
+	 * Drop RTS and DTR.
+	 */
+	if ((tty->termios.c_cflag & CBAUD) == B0) {
+		vs_update_modem_lines(tty, 0, TIOCM_DTR | TIOCM_RTS);
+		mutex_unlock(&local_vsdev->lock);
+		return;
+	}
+
+	/* If coming out of B0, raise DTR and RTS. This might get
+	 * overridden in next steps. Applications like minicom when
+	 * opens a serial port, may drop speed to B0 and then back
+	 * to normal speed again.
+	 */
+	if (!old_termios || (old_termios->c_cflag & CBAUD) == B0) {
+		if (!(tty->termios.c_cflag & CRTSCTS) ||
+				!test_bit(TTY_THROTTLED, &tty->flags)) {
+			mask |= TIOCM_RTS;
+			vs_update_modem_lines(tty, mask, 0);
+		}
+	}
+
+	baud = tty_get_baud_rate(tty);
+	if (!baud)
+		baud = 9600;
+
+	tty_encode_baud_rate(tty, baud, baud);
+
+	local_vsdev->baud = baud;
+
+	uart_frame_settings = 0;
+	if (tty->termios.c_cflag & CRTSCTS) {
+		uart_frame_settings |= VS_CRTSCTS;
+	} else if ((tty->termios.c_iflag & IXON) ||
+					(tty->termios.c_iflag & IXOFF)) {
+		uart_frame_settings |= VS_XON;
+	} else {
+		uart_frame_settings |= VS_NONE;
+	}
+
+	switch (tty->termios.c_cflag & CSIZE) {
+	case CS8:
+		uart_frame_settings |= VS_DATA_8;
+		break;
+	case CS7:
+		uart_frame_settings |= VS_DATA_7;
+		break;
+	case CS6:
+		uart_frame_settings |= VS_DATA_6;
+		break;
+	case CS5:
+		uart_frame_settings |= VS_DATA_5;
+		break;
+	default:
+		uart_frame_settings |= VS_DATA_8;
+	}
+
+	if (tty->termios.c_cflag & CSTOPB)
+		uart_frame_settings |= VS_STOP_2;
+	else
+		uart_frame_settings |= VS_STOP_1;
+
+	if (tty->termios.c_cflag & PARENB) {
+		if (tty->termios.c_cflag & CMSPAR) {
+			if (tty->termios.c_cflag & PARODD)
+				uart_frame_settings |= VS_PARITY_MARK;
+			else
+				uart_frame_settings |= VS_PARITY_SPACE;
+		} else {
+			if (tty->termios.c_cflag & PARODD)
+				uart_frame_settings |= VS_PARITY_ODD;
+			else
+				uart_frame_settings |= VS_PARITY_EVEN;
+		}
+	} else {
+		uart_frame_settings |= VS_PARITY_NONE;
+	}
+
+	local_vsdev->uart_frame = uart_frame_settings;
+
+	mutex_unlock(&local_vsdev->lock);
+}
+
+/*
+ * Returns the number of bytes in device's output queue. This is
+ * invoked when TIOCOUTQ IOCTL is executed or by tty core as and
+ * when required. Because we all push all data into receiver's
+ * end tty buffer, always return 0 here.
+ */
+static int vs_chars_in_buffer(struct tty_struct *tty)
+{
+	return 0;
+}
+
+/*
+ * Based on the number od interrupts check if any of the signal
+ * line has changed.
+ */
+static int vs_check_msr_delta(struct tty_struct *tty,
+		struct vs_dev *local_vsdev, unsigned long mask,
+		struct async_icount *prev)
+{
+	int delta;
+	struct async_icount now;
+
+	/*
+	 * Use tty-port initialised flag to detect all hangups
+	 * including the disconnect(device destroy) event.
+	 */
+	if (!test_bit(ASYNCB_INITIALIZED, &tty->port->flags))
+		return 1;
+
+	mutex_lock(&local_vsdev->lock);
+	now = local_vsdev->icount;
+	mutex_unlock(&local_vsdev->lock);
+	delta = ((mask & TIOCM_RNG && prev->rng != now.rng) ||
+			 (mask & TIOCM_DSR && prev->dsr != now.dsr) ||
+			 (mask & TIOCM_CAR && prev->dcd != now.dcd) ||
+			 (mask & TIOCM_CTS && prev->cts != now.cts));
+
+	*prev = now;
+	return delta;
+}
+
+/* Sleeps until at-least one of the modem lines changes */
+static int vs_wait_change(struct tty_struct *tty, unsigned long mask)
+{
+	int ret;
+	struct async_icount prev;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+
+	local_vsdev->waiting_msr_chg = 1;
+	prev = local_vsdev->icount;
+
+	mutex_unlock(&local_vsdev->lock);
+
+	ret = wait_event_interruptible(tty->port->delta_msr_wait,
+			vs_check_msr_delta(tty, local_vsdev, mask, &prev));
+
+	local_vsdev->waiting_msr_chg = 0;
+
+	if (!ret && !test_bit(ASYNCB_INITIALIZED, &tty->port->flags))
+		ret = -EIO;
+
+	return ret;
+}
+
+/* Execute IOCTL commands */
+static int vs_ioctl(struct tty_struct *tty,
+				unsigned int cmd, unsigned long arg)
+{
+	switch (cmd) {
+	case TIOCGSERIAL:
+		return vs_get_serinfo(tty, arg);
+	case TIOCMIWAIT:
+		return vs_wait_change(tty, arg);
+	}
+
+	return -ENOIOCTLCMD;
+}
+
+/*
+ * Invoked when tty layer's input buffers are about to get full.
+ *
+ * When using RTS/CTS flow control, when RTS line is de-asserted,
+ * interrupt will be generated in hardware. The interrupt handler
+ * will raise a flag to indicate transmission should be stopped.
+ * This is achieved in this driver through tx_paused variable.
+ */
+static void vs_throttle(struct tty_struct *tty)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+	struct vs_dev *remote_vsdev = idr_find(&db, local_vsdev->peer_index);
+
+	if (tty->termios.c_cflag & CRTSCTS) {
+		mutex_lock(&local_vsdev->lock);
+		remote_vsdev->tx_paused = 1;
+		vs_update_modem_lines(tty, 0, TIOCM_RTS);
+		mutex_unlock(&local_vsdev->lock);
+	} else if ((tty->termios.c_iflag & IXON) ||
+				(tty->termios.c_iflag & IXOFF)) {
+		vs_put_char(tty, STOP_CHAR(tty));
+	} else {
+		/* do nothing */
+	}
+}
+
+/*
+ * Invoked when the tty layer's input buffers have been emptied out,
+ * and it now can accept more data. Throttle/Unthrottle is about
+ * notifying remote end to start or stop data as per the currently
+ * active flow control. On the other hand, Start/Stop is about what
+ * action to take at local end itself to start or stop data as per
+ * the currently active flow control.
+ */
+static void vs_unthrottle(struct tty_struct *tty)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+	struct vs_dev *remote_vsdev = idr_find(&db, local_vsdev->peer_index);
+
+	if (tty->termios.c_cflag & CRTSCTS) {
+		/* hardware (RTS/CTS) flow control */
+		mutex_lock(&local_vsdev->lock);
+		remote_vsdev->tx_paused = 0;
+		vs_update_modem_lines(tty, TIOCM_RTS, 0);
+		mutex_unlock(&local_vsdev->lock);
+
+		if (remote_vsdev->own_tty && remote_vsdev->own_tty->port)
+			tty_port_tty_wakeup(remote_vsdev->own_tty->port);
+	} else if ((tty->termios.c_iflag & IXON) ||
+				(tty->termios.c_iflag & IXOFF)) {
+		/* software flow control */
+		vs_put_char(tty, START_CHAR(tty));
+	} else {
+		/* do nothing */
+	}
+}
+
+/*
+ * Invoked when this driver should stop sending data for example
+ * as a part of flow control mechanism.
+ *
+ * Line discipline n_tty calls this function if this device uses
+ * software flow control and an XOFF character is received from
+ * other end.
+ */
+static void vs_stop(struct tty_struct *tty)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+	local_vsdev->tx_paused = 1;
+	mutex_unlock(&local_vsdev->lock);
+}
+
+/*
+ * Invoked when this driver should start sending data for example
+ * as a part of flow control mechanism.
+ *
+ * Line discipline n_tty calls this function if this device uses
+ * software flow control and an XON character is received from
+ * other end.
+ */
+static void vs_start(struct tty_struct *tty)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+	local_vsdev->tx_paused = 0;
+	mutex_unlock(&local_vsdev->lock);
+
+	if (tty && tty->port)
+		tty_port_tty_wakeup(tty->port);
+}
+
+/*
+ * Obtain the modem status bits for the given tty device. Invoked
+ * typically when TIOCMGET IOCTL is executed on the given
+ * tty device.
+ */
+static int vs_tiocmget(struct tty_struct *tty)
+{
+	int status, msr_reg, mcr_reg;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+	mcr_reg = local_vsdev->mcr_reg;
+	msr_reg = local_vsdev->msr_reg;
+	mutex_unlock(&local_vsdev->lock);
+
+	status = ((mcr_reg & VS_MCR_DTR)  ? TIOCM_DTR  : 0) |
+			 ((mcr_reg & VS_MCR_RTS)  ? TIOCM_RTS  : 0) |
+			 ((mcr_reg & VS_MCR_LOOP) ? TIOCM_LOOP : 0) |
+			 ((msr_reg & VS_MSR_DCD)  ? TIOCM_CAR  : 0) |
+			 ((msr_reg & VS_MSR_RI)   ? TIOCM_RI   : 0) |
+			 ((msr_reg & VS_MSR_CTS)  ? TIOCM_CTS  : 0) |
+			 ((msr_reg & VS_MSR_DSR)  ? TIOCM_DSR  : 0);
+
+	return status;
+}
+
+/*
+ * Set the modem status bits. Invoked typically when TIOCMSET IOCTL
+ * is executed on the given tty device.
+ */
+static int vs_tiocmset(struct tty_struct *tty,
+				unsigned int set, unsigned int clear)
+{
+	int ret;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+	ret = vs_update_modem_lines(tty, set, clear);
+	mutex_unlock(&local_vsdev->lock);
+
+	return ret;
+}
+
+/*
+ * Unconditionally assert/de-assert break condition of the given
+ * tty device.
+ */
+static int vs_break_ctl(struct tty_struct *tty, int break_state)
+{
+	struct tty_struct *tty_to_write;
+	struct vs_dev *brk_rx_vsdev;
+	struct vs_dev *brk_tx_vsdev = idr_find(&db, tty->index);
+
+	if (tty->index != brk_tx_vsdev->peer_index) {
+		tty_to_write = brk_tx_vsdev->peer_tty;
+		brk_rx_vsdev = idr_find(&db, brk_tx_vsdev->peer_index);
+	} else {
+		tty_to_write = tty;
+		brk_rx_vsdev = brk_tx_vsdev;
+	}
+
+	mutex_lock(&brk_tx_vsdev->lock);
+
+	if (break_state != 0) {
+		if (brk_tx_vsdev->is_break_on == 1)
+			return 0;
+
+		brk_tx_vsdev->is_break_on = 1;
+		if (tty_to_write != NULL) {
+			tty_insert_flip_char(tty_to_write->port, 0, TTY_BREAK);
+			tty_flip_buffer_push(tty_to_write->port);
+			brk_rx_vsdev->icount.brk++;
+		}
+	} else {
+		brk_tx_vsdev->is_break_on = 0;
+	}
+
+	mutex_unlock(&brk_tx_vsdev->lock);
+	return 0;
+}
+
+/*
+ * Invoked by tty layer to inform this driver that it should hangup
+ * the tty device (lower modem control lines after last process
+ * using tty devices closes the device or exited).
+ *
+ * Drop DTR/RTS if HUPCL is set. This causes any attached modem to
+ * hang up the line.
+ *
+ * On the receiving end, if CLOCAL bit is set, DCD will be ignored
+ * otherwise SIGHUP may be generated to indicate a line disconnect
+ * event.
+ */
+static void vs_hangup(struct tty_struct *tty)
+{
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+
+	/* Drops reference to tty */
+	tty_port_hangup(tty->port);
+
+	if (tty && C_HUPCL(tty))
+		vs_update_modem_lines(tty, 0, TIOCM_DTR | TIOCM_RTS);
+
+	mutex_unlock(&local_vsdev->lock);
+	pr_debug("hanged up!\n");
+}
+
+/*
+ * Return number of interrupts as response to TIOCGICOUNT IOCTL.
+ * Both 1->0 and 0->1 transitions are counted, except for RI;
+ * where only 0->1 transitions are accounted.
+ */
+static int vs_get_icount(struct tty_struct *tty,
+				struct serial_icounter_struct *icount)
+{
+	struct async_icount cnow;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	mutex_lock(&local_vsdev->lock);
+	cnow = local_vsdev->icount;
+	mutex_unlock(&local_vsdev->lock);
+
+	icount->cts = cnow.cts;
+	icount->dsr = cnow.dsr;
+	icount->rng = cnow.rng;
+	icount->dcd = cnow.dcd;
+	icount->tx = cnow.tx;
+	icount->rx = cnow.rx;
+	icount->frame = cnow.frame;
+	icount->parity = cnow.parity;
+	icount->overrun = cnow.overrun;
+	icount->brk = cnow.brk;
+	icount->buf_overrun = cnow.buf_overrun;
+
+	return 0;
+}
+
+/*
+ * Invoked by tty layer to execute TCIOFF and TCION IOCTL commands
+ * generally because user space process called tcflow() function.
+ * It send a high priority character to the tty device end even if
+ * stopped.
+ *
+ * If this function (send_xchar) is defined by tty device driver,
+ * tty core will call this function. If it is not specified then
+ * tty core will first instruct this driver to start transmission
+ * (start()) and then invoke write() of this driver passing character
+ * to be written and then it will call stop() of this driver.
+ */
+static void vs_send_xchar(struct tty_struct *tty, char ch)
+{
+	int was_paused;
+	struct vs_dev *local_vsdev = idr_find(&db, tty->index);
+
+	was_paused = local_vsdev->tx_paused;
+	if (was_paused)
+		local_vsdev->tx_paused = 0;
+
+	vs_put_char(tty, ch);
+	if (was_paused)
+		local_vsdev->tx_paused = 1;
+}
+
+/*
+ * Invoked by tty core in response to tcdrain() call. As this driver
+ * drains on write() itself, we return immediately from here.
+ */
+static void vs_wait_until_sent(struct tty_struct *tty, int timeout)
+{
+	pr_debug("returned wait until sent!\n");
+}
+
+/*
+ * Unregister tty device specified by minor number ownidx
+ * and remove sysfs files associate with it. Caller must
+ * hold card lock. First tty must be released and then port.
+ *
+ * It is common to reset environment before launching new test
+ * suite during automated testing. To support this we allow
+ * removing devices even when it was created using DT as of
+ * now till we find any valid reason not do so.
+ */
+static void vs_unreg_one_dev(int ownidx, struct vs_dev *vsdev)
+{
+	struct tty_struct *tty;
+
+	if (vsdev->own_tty && vsdev->own_tty->port) {
+		tty = tty_port_tty_get(vsdev->own_tty->port);
+		if (tty) {
+			tty_vhangup(tty);
+			tty_kref_put(tty);
+		}
+	}
+
+	tty_unregister_device(ttyvs_driver, ownidx);
+}
+
+/*
+ * Destroy a virtual tty device specified by the given index.
+ * Whether IDR id will be freed or not is specified by the
+ * caller through free_idr.
+ */
+static int vs_del_specific_devs(int ownidx, int free_idr)
+{
+	struct vs_dev *vsdev1, *vsdev2;
+
+	/*
+	 * If user just created configfs item but did not populated valid
+	 * index, device will not exist, so bail out early.
+	 */
+	vsdev1 = idr_find(&db, ownidx);
+	if (!vsdev1)
+		return 0;
+
+	vs_unreg_one_dev(ownidx, vsdev1);
+
+	/* If this device is part of a null modem, delete peer also */
+	if (vsdev1->own_index != vsdev1->peer_index) {
+		vsdev2 = idr_find(&db, vsdev1->peer_index);
+		if (vsdev2) {
+			vs_unreg_one_dev(vsdev2->own_index, vsdev2);
+			if (free_idr)
+				idr_remove(&db, vsdev2->own_index);
+			kfree(vsdev2);
+		}
+	}
+
+	if (free_idr)
+		idr_remove(&db, ownidx);
+	kfree(vsdev1);
+
+	return 0;
+}
+
+/*
+ * Destroy all tty devices created, mark all the indexes as
+ * available for allocation; reset IDR for re-use.
+ */
+static void vs_del_all_devs(void)
+{
+	int x;
+	struct vs_dev *vsdev;
+
+	mutex_lock(&card_lock);
+
+	idr_for_each_entry(&db, vsdev, x)
+		vs_del_specific_devs(vsdev->own_index, 0);
+
+	idr_destroy(&db);
+
+	mutex_unlock(&card_lock);
+}
+
+/*
+ * Allocate per device private data (vsdev) for this driver, register
+ * with tty core and create custom sysfs nodes for emulating serial
+ * port events. Caller should hold card lock.
+ */
+static int vs_alloc_reg_one_dev(int oidx, int pidx, int rtsmap,
+			int dtrmap, int dtropn)
+{
+	int ret, id;
+	struct vs_dev *vsdev;
+	struct device *dev;
+
+	/* Allocate and init virtual tty device's private data */
+	vsdev = kcalloc(1, sizeof(struct vs_dev), GFP_KERNEL);
+	if (!vsdev)
+		return -ENOMEM;
+
+	id = idr_alloc(&db, vsdev, oidx, oidx + 1, GFP_KERNEL);
+	if (id < 0) {
+		ret = id;
+		goto fail_id;
+	}
+
+	vsdev->own_tty = NULL;
+	vsdev->peer_tty = NULL;
+	vsdev->own_index = oidx;
+	vsdev->peer_index =  pidx;
+	vsdev->rts_mappings = rtsmap;
+	vsdev->dtr_mappings = dtrmap;
+	vsdev->set_odtr_at_open = dtropn;
+	vsdev->msr_reg = 0;
+	vsdev->mcr_reg = 0;
+	vsdev->waiting_msr_chg = 0;
+	vsdev->tx_paused = 0;
+	vsdev->faulty_cable = 0;
+	mutex_init(&vsdev->lock);
+
+	/*
+	 * Register with tty core with a specific minor number.
+	 * Driver core itself will create sysfs nodes (ttyvs_groups).
+	 */
+	dev = tty_register_device_attr(ttyvs_driver, oidx, NULL,
+				vsdev, ttyvs_groups);
+	if (!dev) {
+		ret = -ENOMEM;
+		goto fail_reg;
+	}
+
+	vsdev->device = dev;
+	return 0;
+
+fail_reg:
+	idr_remove(&db, id);
+fail_id:
+	kfree(vsdev);
+	return ret;
+}
+
+/*
+ * Extract pin mappings from local to remote tty devices.
+ * The map contains bits setted by user. Returns 0 on success
+ * or negative error code on error. The *mapping will contain
+ * pin connections (bit map as used by this driver) when this
+ * function returns.
+ */
+static int vs_extract_pin_mapping(int usrval, int *mapping)
+{
+	if (usrval > (VS_CON_CTS | VS_CON_DCD | VS_CON_DSR | VS_CON_RI))
+		return -EINVAL;
+
+	/* No pin connections by-default */
+	*mapping = 0;
+
+	if ((usrval & VS_CON_CTS) == VS_CON_CTS)
+		*mapping |= VS_CON_CTS;
+
+	if ((usrval & VS_CON_DCD) == VS_CON_DCD)
+		*mapping |= VS_CON_DCD;
+
+	if ((usrval & VS_CON_DSR) == VS_CON_DSR)
+		*mapping |= VS_CON_DSR;
+
+	if ((usrval & VS_CON_RI) == VS_CON_RI)
+		*mapping |= VS_CON_RI;
+
+	return 0;
+}
+
+/*
+ * The devtyp is 1 for null modem and 0 for loop-back. We extract
+ * user supplied information, validate it and convert it as
+ * required by this driver to create a device.
+ */
+static int vs_extract_dev_param_cfs(const struct vs_cfs_dev_info *di,
+			unsigned int *idx, int *rtsmap, int *dtrmap,
+			int *dtratopen, int devtyp)
+{
+	int ret;
+
+	if (devtyp) {
+		if (di->peeridx >= max_num_vs_devs)
+			return -EINVAL;
+
+		*idx = di->peeridx;
+
+		ret = vs_extract_pin_mapping(di->prtsmap, rtsmap);
+		if (ret)
+			return ret;
+
+		ret = vs_extract_pin_mapping(di->pdtrmap, rtsmap);
+		if (ret)
+			return ret;
+
+		*dtratopen = di->pdtratopn ? 1 : 0;
+	} else {
+		if (di->ownidx >= max_num_vs_devs)
+			return -EINVAL;
+
+		*idx = di->ownidx;
+
+		ret = vs_extract_pin_mapping(di->ortsmap, rtsmap);
+		if (ret)
+			return ret;
+
+		ret = vs_extract_pin_mapping(di->odtrmap, rtsmap);
+		if (ret)
+			return ret;
+
+		*dtratopen = di->odtratopn  ? 1 : 0;
+	}
+
+	return 0;
+}
+
+/* Converts pin mappings from dt node to this driver specific bit map */
+static int vs_parse_dt_get_map(const struct device_node *np,
+				const char *prop, int *mapping)
+{
+	int x, ret, num_map;
+	int val[4];
+
+	/*
+	 * If the RTS/DTR pin is unconnected (property doesn't exist)
+	 * set mapping to 0 and return success.
+	 */
+	ret = of_property_count_u32_elems(np, prop);
+	if (ret < 0) {
+		if (ret == -EINVAL) {
+			*mapping = 0;
+			return 0;
+		}
+		return ret;
+	}
+
+	/*
+	 * A given pin can be connected to 1,6,8,9 pins. Therefore if
+	 * more then 4 mappings are defined in DT, ignore it.
+	 */
+	num_map = ret;
+	if (ret > 4)
+		num_map = 4;
+
+	ret = of_property_read_u32_array(np, prop, val, num_map);
+	if (ret < 0)
+		return ret;
+
+	*mapping = 0;
+	for (x = 0; x < num_map; x++) {
+		switch (val[x]) {
+		case 8:
+			*mapping |= VS_CON_CTS;
+			break;
+		case 1:
+			*mapping |= VS_CON_DCD;
+			break;
+		case 6:
+			*mapping |= VS_CON_DSR;
+			break;
+		case 9:
+			*mapping |= VS_CON_RI;
+			break;
+		default:
+			return -EINVAL;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Extract index of device, RTS mappings, DTR mappings and
+ * whether to assert DTR at device open or not from dt node.
+ */
+static int vs_extract_dev_param_dt(const struct device_node *np,
+			unsigned int *idx, int *rtsmap, int *dtrmap,
+			int *dtratopen, int exclude)
+{
+	int ret;
+
+	ret = of_property_read_u32(np, "dev-num", idx);
+	if (ret)
+		return ret;
+
+	if (*idx >= max_num_vs_devs)
+		return -EINVAL;
+
+	ret = vs_parse_dt_get_map(np, "rtsmap", rtsmap);
+	if (ret)
+		return ret;
+
+	ret = vs_parse_dt_get_map(np, "dtrmap", dtrmap);
+	if (ret)
+		return ret;
+
+	*dtratopen = of_property_read_bool(np,
+						"set-dtr-at-open") ? 1 : 0;
+
+	return 0;
+}
+
+/*
+ * Create a loop-back style device:
+ *
+ * 0. Information about device parameters can come through either
+ *    configfs node or device-tree node.
+ * 1. Decide index to use; the number is specified by user. If the
+ *    given index is used already through error.
+ * 2. Extract RTS and DTR mappings. A pin can map to pin numbers
+ *    1,6,8,9 only or might be un-connected. Through error if
+ *    invalid mapping is given.
+ * 3. Find if DTR should be asserted when tty device is opened or
+ *    not.
+ * 4. Allocate and initialize 'struct vs_dev' instance with info
+ *    from steps 1,2 & 3.
+ * 5. Register one tty device with tty core and associate this tty
+ *    device with vsdev instance from step 4.
+ * 6. Create custom sysfs nodes to emulate serial port events for
+ *    this device.
+ */
+static int vs_add_lb(const struct vs_cfs_dev_info *di,
+				const struct device_node *np)
+{
+	int ret, rtsmap, dtrmap, dtratopen;
+	unsigned int idx;
+
+	mutex_lock(&card_lock);
+
+	if (di) {
+		ret = vs_extract_dev_param_cfs(di, &idx, &rtsmap,
+					&dtrmap, &dtratopen, 0);
+	} else {
+		ret = vs_extract_dev_param_dt(np, &idx, &rtsmap,
+					&dtrmap, &dtratopen, -1);
+	}
+	if (ret)
+		goto fail;
+
+	ret = vs_alloc_reg_one_dev(idx, idx, rtsmap, dtrmap, dtratopen);
+	if (ret)
+		goto fail;
+
+fail:
+	mutex_unlock(&card_lock);
+	return ret;
+}
+
+/*
+ * Create a null-modem style pair of devices:
+ *
+ * Steps are same as for creating loop-back style device except,
+ * we create both the devices on success or none of them on error.
+ */
+static int vs_add_nm(const struct vs_cfs_dev_info *di,
+				const struct device_node *np1,
+				const struct device_node *np2)
+{
+	int ret, rtsmap1, dtrmap1, dtratopen1;
+	int rtsmap2, dtrmap2, dtratopen2;
+	unsigned int idx1, idx2;
+
+	mutex_lock(&card_lock);
+
+	if (di) {
+		ret = vs_extract_dev_param_cfs(di, &idx1, &rtsmap1, &dtrmap1,
+				&dtratopen1, 0);
+		if (ret)
+			goto out;
+
+		ret = vs_extract_dev_param_cfs(di, &idx2, &rtsmap2, &dtrmap2,
+				&dtratopen2, 1);
+	} else {
+		ret = vs_extract_dev_param_dt(np1, &idx1, &rtsmap1,
+					&dtrmap1, &dtratopen1, -1);
+		if (ret)
+			goto out;
+
+		ret = vs_extract_dev_param_dt(np2, &idx2, &rtsmap2,
+					&dtrmap2, &dtratopen2, idx1);
+	}
+	if (ret)
+		goto out;
+
+	ret = vs_alloc_reg_one_dev(idx1, idx2, rtsmap1, dtrmap1, dtratopen1);
+	if (ret)
+		goto out;
+
+	ret = vs_alloc_reg_one_dev(idx2, idx1, rtsmap2, dtrmap2, dtratopen2);
+	if (ret)
+		vs_del_specific_devs(idx1, 1);
+
+out:
+	mutex_unlock(&card_lock);
+	return ret;
+}
+
+static const struct tty_operations vs_serial_ops = {
+	.install	     = vs_install,
+	.cleanup	     = vs_cleanup,
+	.open	         = vs_open,
+	.close	         = vs_close,
+	.write	         = vs_write,
+	.put_char	     = vs_put_char,
+	.flush_chars     = vs_flush_chars,
+	.write_room      = vs_write_room,
+	.chars_in_buffer = vs_chars_in_buffer,
+	.ioctl	         = vs_ioctl,
+	.set_termios     = vs_set_termios,
+	.throttle	     = vs_throttle,
+	.unthrottle      = vs_unthrottle,
+	.stop	         = vs_stop,
+	.start	         = vs_start,
+	.hangup	         = vs_hangup,
+	.break_ctl       = vs_break_ctl,
+	.flush_buffer    = vs_flush_buffer,
+	.wait_until_sent = vs_wait_until_sent,
+	.send_xchar      = vs_send_xchar,
+	.tiocmget	     = vs_tiocmget,
+	.tiocmset	     = vs_tiocmset,
+	.get_icount      = vs_get_icount,
+};
+
+static int vs_register_with_tty_core(void)
+{
+	int ret;
+
+	/* Initialize and register this driver with tty core */
+	ttyvs_driver = tty_alloc_driver(max_num_vs_devs, 0);
+	if (IS_ERR(ttyvs_driver))
+		return PTR_ERR(ttyvs_driver);
+
+	ttyvs_driver->owner = THIS_MODULE;
+	ttyvs_driver->driver_name = "ttyvs";
+	ttyvs_driver->name = "ttyvs";
+	ttyvs_driver->major = 0;
+	ttyvs_driver->minor_start = 0;
+	ttyvs_driver->type = TTY_DRIVER_TYPE_SERIAL;
+	ttyvs_driver->subtype = SERIAL_TYPE_NORMAL;
+	ttyvs_driver->flags = TTY_DRIVER_REAL_RAW
+				| TTY_DRIVER_RESET_TERMIOS
+				| TTY_DRIVER_DYNAMIC_DEV;
+	ttyvs_driver->init_termios = tty_std_termios;
+	ttyvs_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL;
+	ttyvs_driver->init_termios.c_ispeed = 9600;
+	ttyvs_driver->init_termios.c_ospeed = 9600;
+
+	tty_set_operations(ttyvs_driver, &vs_serial_ops);
+
+	ret = tty_register_driver(ttyvs_driver);
+	if (ret)
+		put_tty_driver(ttyvs_driver);
+
+	return ret;
+}
+
+/*
+ * Information passed through device tree is given more preference
+ * then through module params. This parses all device nodes and
+ * creates loop-back and null-modem ttyvsX devices in the process.
+ */
+static int ttyvs_device_probe(struct platform_device *pdev)
+{
+	int ret;
+	u32 max_num;
+	struct device_node *child, *peer_node;
+	phandle peer;
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+
+	/*
+	 * We register with tty core again only if maximum number of
+	 * devices registered during module_init is changed by device
+	 * tree.
+	 */
+	max_num = 0;
+	ret = of_property_read_u32(np, "max-num-vs-devs", &max_num);
+	if (!ret && (max_num != max_num_vs_devs)) {
+		tty_unregister_driver(ttyvs_driver);
+		put_tty_driver(ttyvs_driver);
+
+		max_num_vs_devs = max_num;
+		ret = vs_register_with_tty_core();
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * If we fail to create any device emit error log and move to
+	 * the next dt node.
+	 */
+	for_each_available_child_of_node(np, child) {
+		if (of_node_test_and_set_flag(child, OF_POPULATED))
+			continue;
+
+		if (of_property_read_u32(child, "peer-dev", &peer)) {
+			ret = vs_add_lb(NULL, child);
+			if (ret) {
+				pr_err("can't create lb %s %d\n",
+						child->name, ret);
+				continue;
+			}
+		} else {
+			peer_node = of_find_node_by_phandle(peer);
+			if (peer_node) {
+				of_node_set_flag(peer_node, OF_POPULATED);
+				ret = vs_add_nm(NULL, child, peer_node);
+				if (ret) {
+					pr_err("can't create nm %s <-> %s %d\n",
+						child->name, peer_node->name,
+						ret);
+					continue;
+				}
+			} else {
+				pr_err("can't find peer for %s %d\n",
+						child->name, ret);
+			}
+		}
+	}
+
+	return 0;
+}
+
+static inline struct vs_cfs_dev_info *to_vs_dinfo(
+				struct config_item *item)
+{
+	return container_of(to_config_group(item),
+				struct vs_cfs_dev_info, grp);
+}
+
+#define VS_DEV_ATTR_WR_U8(_name) \
+static ssize_t vs_dev_##_name##_store(struct config_item *item, \
+		const char *page, size_t len) \
+{                                     \
+	u8 val;                           \
+	int ret;                          \
+	ret = kstrtou8(page, 0, &val);    \
+	if (ret)                          \
+		return ret;                   \
+	to_vs_dinfo(item)->_name = val;   \
+	return len;                       \
+}                                     \
+static ssize_t vs_dev_##_name##_show(struct config_item *item, \
+		char *buf)                    \
+{                                     \
+	return snprintf(buf, PAGE_SIZE, "%u\n", to_vs_dinfo(item)->_name); \
+}
+
+#define VS_DEV_ATTR_WR_U16(_name)     \
+static ssize_t vs_dev_##_name##_store(struct config_item *item, \
+		const char *page, size_t len) \
+{                                     \
+	u16 val;                          \
+	int ret;                          \
+	ret = kstrtou16(page, 0, &val);   \
+	if (ret)                          \
+		return ret;                   \
+	to_vs_dinfo(item)->_name = val;   \
+	return len;                       \
+}                                     \
+static ssize_t vs_dev_##_name##_show(struct config_item *item, \
+		char *buf)                     \
+{                                      \
+	return snprintf(buf, PAGE_SIZE, "%u\n", to_vs_dinfo(item)->_name); \
+}
+
+#define VS_DEV_ATTR_WR_STR(_name) \
+static ssize_t vs_dev_##_name##_store(struct config_item *item, \
+		const char *page, size_t len)     \
+{                                         \
+	char *devtype;                        \
+	devtype = kstrdup(page, GFP_KERNEL);  \
+	if (!devtype)                         \
+		return -ENOMEM;                   \
+	if (devtype[len - 1] == '\n')         \
+		devtype[len - 1] = '\0';          \
+	to_vs_dinfo(item)->devtype = devtype; \
+	return len;                           \
+}                                         \
+static ssize_t vs_dev_##_name##_show(struct config_item *item, \
+		char *buf)                        \
+{                                         \
+	return snprintf(buf, PAGE_SIZE, "%s\n", to_vs_dinfo(item)->devtype); \
+}
+
+/*
+ * Once all parameters for the device has been set, this finally
+ * creates the device.
+ */
+static ssize_t vs_dev_create_store(struct config_item *item,
+		const char *page, size_t len)
+{
+	u8 val;
+	int ret;
+	struct vs_cfs_dev_info *di;
+
+	ret = kstrtou8(page, 0, &val);
+	if (ret)
+		return ret;
+
+	/* User must write 1 to this node create device */
+	if (val != 1)
+		return -EINVAL;
+
+	di = to_vs_dinfo(item);
+
+	/* devtype must be defined to proceed further */
+	if (!di->devtype)
+		return -EINVAL;
+
+	if (strncmp(di->devtype, "lb", 2) == 0)
+		ret = vs_add_lb(di, NULL);
+	else if (strncmp(di->devtype, "nm", 2) == 0)
+		ret = vs_add_nm(di, NULL, NULL);
+	else
+		return -EINVAL;
+
+	if (ret)
+		return ret;
+	return len;
+}
+
+VS_DEV_ATTR_WR_STR(devtype)
+VS_DEV_ATTR_WR_U16(ownidx)
+VS_DEV_ATTR_WR_U16(peeridx)
+VS_DEV_ATTR_WR_U8(ortsmap)
+VS_DEV_ATTR_WR_U8(odtrmap)
+VS_DEV_ATTR_WR_U8(odtratopn)
+VS_DEV_ATTR_WR_U8(prtsmap)
+VS_DEV_ATTR_WR_U8(pdtrmap)
+VS_DEV_ATTR_WR_U8(pdtratopn)
+
+CONFIGFS_ATTR(vs_dev_, devtype);
+CONFIGFS_ATTR(vs_dev_, ownidx);
+CONFIGFS_ATTR(vs_dev_, ortsmap);
+CONFIGFS_ATTR(vs_dev_, odtrmap);
+CONFIGFS_ATTR(vs_dev_, odtratopn);
+CONFIGFS_ATTR(vs_dev_, peeridx);
+CONFIGFS_ATTR(vs_dev_, prtsmap);
+CONFIGFS_ATTR(vs_dev_, pdtrmap);
+CONFIGFS_ATTR(vs_dev_, pdtratopn);
+CONFIGFS_ATTR_WO(vs_dev_, create);
+
+static struct configfs_attribute *vs_dev_attrs[] = {
+	&vs_dev_attr_devtype,
+	&vs_dev_attr_ownidx,
+	&vs_dev_attr_ortsmap,
+	&vs_dev_attr_odtrmap,
+	&vs_dev_attr_odtratopn,
+	&vs_dev_attr_peeridx,
+	&vs_dev_attr_prtsmap,
+	&vs_dev_attr_pdtrmap,
+	&vs_dev_attr_pdtratopn,
+	&vs_dev_attr_create,
+	NULL,
+};
+
+static const struct config_item_type vs_cfs_root_type = {
+	.ct_attrs = vs_dev_attrs,
+	.ct_owner = THIS_MODULE,
+};
+
+static struct config_group *vs_cfs_grp_make(
+			struct config_group *group,
+			const char *name)
+{
+	struct vs_cfs_dev_info *di;
+
+	di = kzalloc(sizeof(*di), GFP_KERNEL);
+	if (!di)
+		return ERR_PTR(-ENOMEM);
+
+	config_group_init_type_name(&di->grp, name, &vs_cfs_root_type);
+
+	return &di->grp;
+}
+
+static void vs_cfs_grp_drop(struct config_group *group,
+				struct config_item *item)
+{
+	struct vs_cfs_dev_info *di = to_vs_dinfo(item);
+
+	mutex_lock(&card_lock);
+	vs_del_specific_devs(di->ownidx, 1);
+	mutex_unlock(&card_lock);
+
+	kfree(di);
+	config_item_put(item);
+}
+
+static struct configfs_group_operations vs_cfs_grp_ops = {
+	.make_group     = &vs_cfs_grp_make,
+	.drop_item      = &vs_cfs_grp_drop,
+};
+
+static const struct config_item_type vs_cfs_grp_type = {
+	.ct_group_ops = &vs_cfs_grp_ops,
+	.ct_owner = THIS_MODULE,
+};
+
+struct configfs_subsystem vs_cfs_subsys = {
+	.su_group = {
+		.cg_item = {
+			.ci_namebuf = "ttyvs",
+			.ci_type = &vs_cfs_grp_type,
+		},
+	},
+	.su_mutex = __MUTEX_INITIALIZER(vs_cfs_subsys.su_mutex),
+};
+
+static const struct of_device_id ttyvs_dev_match_tbl[] = {
+	{ .compatible = "ttyvs,virtual-uart-card" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, ttyvs_dev_match_tbl);
+
+static struct platform_driver ttyvs_platform_drv = {
+	.probe		= ttyvs_device_probe,
+	.driver		= {
+		.name	= "ttyvs",
+		.of_match_table = ttyvs_dev_match_tbl,
+	},
+};
+
+static int __init ttyvs_init(void)
+{
+	int ret;
+
+	config_group_init(&vs_cfs_subsys.su_group);
+
+	ret = configfs_register_subsystem(&vs_cfs_subsys);
+	if (ret)
+		return ret;
+
+	ret = vs_register_with_tty_core();
+	if (ret)
+		goto fail_drv;
+
+	/* Register as platform driver to handle device tree nodes */
+	ret = platform_driver_register(&ttyvs_platform_drv);
+	if (ret)
+		goto fail_plat;
+
+	pr_info("serial port null modem emulation driver\n");
+	return 0;
+
+fail_plat:
+	tty_unregister_driver(ttyvs_driver);
+	put_tty_driver(ttyvs_driver);
+
+fail_drv:
+	configfs_unregister_subsystem(&vs_cfs_subsys);
+
+	return ret;
+}
+
+static void __exit ttyvs_exit(void)
+{
+	vs_del_all_devs();
+
+	configfs_unregister_subsystem(&vs_cfs_subsys);
+	platform_driver_unregister(&ttyvs_platform_drv);
+
+	tty_unregister_driver(ttyvs_driver);
+	put_tty_driver(ttyvs_driver);
+}
+
+module_init(ttyvs_init);
+module_exit(ttyvs_exit);
+
+/*
+ * By default this driver supports upto 64 virtual devices. This
+ * can be overridden through max_num_vs_devs module parameter or
+ * through max-num-vs-devs device tree property.
+ */
+module_param(max_num_vs_devs, ushort, 0);
+MODULE_PARM_DESC(max_num_vs_devs,
+		"Maximum virtual tty devices to be supported");
+
+MODULE_AUTHOR("Rishi Gupta <gupt21@gmail.com>");
+MODULE_DESCRIPTION("Serial port null modem emulation driver");
+MODULE_LICENSE("GPL v2");