[PATCH v6] USB: serial: nct_usb_serial: add support for Nuvoton USB adapter

hsyemail2@gmail.com posted 1 patch 2 weeks, 6 days ago
drivers/usb/serial/Kconfig          |   10 +
drivers/usb/serial/Makefile         |    1 +
drivers/usb/serial/nct_usb_serial.c | 1455 +++++++++++++++++++++++++++
3 files changed, 1466 insertions(+)
create mode 100644 drivers/usb/serial/nct_usb_serial.c
[PATCH v6] USB: serial: nct_usb_serial: add support for Nuvoton USB adapter
Posted by hsyemail2@gmail.com 2 weeks, 6 days ago
From: Sheng-Yuan Huang <syhuang3@nuvoton.com>

Add support for the Nuvoton USB-to-serial adapter, which provides
multiple serial ports over a single USB interface.

The device exposes one control endpoint, one bulk-in endpoint, and
one bulk-out endpoint for data transfer. Port status is reported via
an interrupt-in or bulk-in endpoint, depending on device configuration.

This driver implements basic TTY operations.

Signed-off-by: Sheng-Yuan Huang <syhuang3@nuvoton.com>
---

v6:
- Address review comments from Oliver Neukum
- Thanks to Oliver Neukum and Johan Hovold for their review
- Narrow the protected scope in nct_tiocmset_helper(): port_lock now
  protects only cached HCR updates, while command transmission is
  serialized separately.
- Serialize the open transaction to prevent concurrent open calls
  from returning success before startup completes or before the open
  command is sent.
- Serialize the close transaction to avoid resource races.

 drivers/usb/serial/Kconfig          |   10 +
 drivers/usb/serial/Makefile         |    1 +
 drivers/usb/serial/nct_usb_serial.c | 1455 +++++++++++++++++++++++++++
 3 files changed, 1466 insertions(+)
 create mode 100644 drivers/usb/serial/nct_usb_serial.c

diff --git a/drivers/usb/serial/Kconfig b/drivers/usb/serial/Kconfig
index ef8d1c73c754..96a022bb9b21 100644
--- a/drivers/usb/serial/Kconfig
+++ b/drivers/usb/serial/Kconfig
@@ -443,6 +443,16 @@ config USB_SERIAL_NAVMAN
 	  To compile this driver as a module, choose M here: the
 	  module will be called navman.
 
+config USB_SERIAL_NUV_MULTI_UART
+	tristate "USB Nuvoton Multi-Ports Serial Driver"
+	depends on USB_SERIAL
+	help
+	Say Y here if you want to use a Nuvoton Multi-Ports USB to
+	serial converter device
+	
+	To compile this driver as a module, choose M here: the
+	module will be called nct_usb_serial.
+
 config USB_SERIAL_PL2303
 	tristate "USB Prolific 2303 Single Port Serial Driver"
 	help
diff --git a/drivers/usb/serial/Makefile b/drivers/usb/serial/Makefile
index c7bb1a88173e..c07919a52076 100644
--- a/drivers/usb/serial/Makefile
+++ b/drivers/usb/serial/Makefile
@@ -41,6 +41,7 @@ obj-$(CONFIG_USB_SERIAL_MOS7720)		+= mos7720.o
 obj-$(CONFIG_USB_SERIAL_MOS7840)		+= mos7840.o
 obj-$(CONFIG_USB_SERIAL_MXUPORT)		+= mxuport.o
 obj-$(CONFIG_USB_SERIAL_NAVMAN)			+= navman.o
+obj-$(CONFIG_USB_SERIAL_NUV_MULTI_UART)		+= nct_usb_serial.o
 obj-$(CONFIG_USB_SERIAL_OMNINET)		+= omninet.o
 obj-$(CONFIG_USB_SERIAL_OPTICON)		+= opticon.o
 obj-$(CONFIG_USB_SERIAL_OPTION)			+= option.o
diff --git a/drivers/usb/serial/nct_usb_serial.c b/drivers/usb/serial/nct_usb_serial.c
new file mode 100644
index 000000000000..89dbfdc92136
--- /dev/null
+++ b/drivers/usb/serial/nct_usb_serial.c
@@ -0,0 +1,1455 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2024-2025 Nuvoton Corp
+ * Copyright (C) 2024-2025 Sheng-Yuan Huang <syhuang3@nuvoton.com>
+ *
+ * Nuvoton USB to serial adapter driver
+ *
+ * This device interface consists of one control endpoint for configuration,
+ * one bulk-out endpoint used for transmitting data for all serial ports,
+ * and one bulk-in endpoint for receiving data from all serial ports.
+ * The status of the ports may be reported via either an interrupt endpoint
+ * or the bulk-in endpoint, depending on the device configuration.
+ *
+ * The number of serial ports is configurable in firmware and reported to
+ * the driver via a vendor command at probe time.
+ *
+ * Host-device handshake sequence:
+ *   1. Host sends SET_INIT to initialize the device.
+ *   2. Host sends GET_PORTS_SUPPORT to query enabled ports (bitmask).
+ *   3. For each port open: Host sends SET_OPEN_PORT.
+ *   4. Host sends SET_CONFIG/SET_BAUD to configure UART parameters.
+ *   5. For each port close: Host sends SET_CLOSE_PORT.
+ */
+
+#include <linux/bitops.h>
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/serial.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/tty.h>
+#include <linux/tty_driver.h>
+#include <linux/tty_port.h>
+#include <linux/tty_flip.h>
+#include <linux/uaccess.h>
+#include <linux/usb.h>
+#include <linux/usb/serial.h>
+#include <linux/kfifo.h>
+#include <linux/mutex.h>
+
+#define NCT_WRITE_FIFO_SIZE 1024
+
+#define NCT_VENDOR_ID			0x0416
+#define NCT_PRODUCT_ID			0x200b
+#define NCT_USB_CLASS			0xff
+#define NCT_USB_SUBCLASS		0x0
+#define NCT_USB_PROTOCOL		0x1
+
+#define NCT_MAX_VENDOR_READ_SIZE	8
+
+static const struct usb_device_id id_table[] = {
+	{USB_DEVICE_AND_INTERFACE_INFO(NCT_VENDOR_ID, NCT_PRODUCT_ID,
+				       NCT_USB_CLASS, NCT_USB_SUBCLASS,
+				       NCT_USB_PROTOCOL)},
+	{} /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, id_table);
+
+#define NCT_MAX_SEND_BULK_SIZE 128
+/* The port does not exist in FW (for device status). */
+#define NCT_EMPTY_PORT	0xff
+
+/* Hardware configuration */
+#define NCT_MAX_NUM_COM_DEVICES		8
+
+#define NCT_DEFAULT_BAUD		14 /* 115200 */
+static const unsigned int NCT_BAUD_SUP[] = {
+	/* It should be the same as FW's baud-rate table. */
+	B0,	B50,	B75,	B150,	B300,	B600,	B1200,
+	B1800,	B2400,	B4800,	B9600,	B19200,	B38400,	B57600,
+	B115200, B230400, B460800, B921600, B1500000
+};
+
+/* USB request */
+#define NCT_VENDOR_COM_READ_REQUEST_TYPE	0xc0
+#define NCT_VENDOR_COM_WRITE_REQUEST_TYPE	0x40
+#define NCT_VENDOR_COM_READ_REQUEST		0x01
+#define NCT_VENDOR_COM_WRITE_REQUEST		0x01
+/* Index definition */
+#define NCT_VCOM_INDEX_GLOBAL	0x0f
+
+/* Command */
+#define NCT_VCOM_GET_NUM_PORTS		0
+#define NCT_VCOM_GET_PORTS_SUPPORT	1
+#define NCT_VCOM_GET_BAUD		2
+#define NCT_VCOM_SET_INIT		3
+#define NCT_VCOM_SET_CONFIG		4
+#define NCT_VCOM_SET_BAUD		5
+#define NCT_VCOM_SET_HCR		6
+#define NCT_VCOM_SET_OPEN_PORT		7
+#define NCT_VCOM_SET_CLOSE_PORT		8
+#define NCT_VCOM_SILENT			9
+/* Use bulk-in status instead of interrupt-in status */
+#define NCT_VCON_SET_BULK_IN_STATUS	10
+
+struct nct_vendor_cmd {
+	/* bits[3:0]: index, bits[11:4]: cmd, bits[15:12]: reserved */
+	__le16 val;
+};
+
+#define NCT_CMD_INDEX_MASK	0x000f
+#define NCT_CMD_CMD_MASK	0x0ff0
+#define NCT_CMD_CMD_SHIFT	4
+
+static inline __le16 nct_build_cmd(u8 cmd_code, u8 index)
+{
+	return cpu_to_le16(((u16)cmd_code << NCT_CMD_CMD_SHIFT) |
+			   (index & NCT_CMD_INDEX_MASK));
+}
+
+static inline u8 nct_get_cmd_index(__le16 val)
+{
+	return le16_to_cpu(val) & NCT_CMD_INDEX_MASK;
+}
+
+static inline u8 nct_get_cmd_cmd(__le16 val)
+{
+	return (le16_to_cpu(val) & NCT_CMD_CMD_MASK) >> NCT_CMD_CMD_SHIFT;
+}
+
+#define NCT_HDR_MAGIC		0xa5
+#define NCT_HDR_MAGIC2		0x5a
+#define NCT_HDR_MAGIC_STATUS	0x5b
+
+struct nct_packet_header {
+	u8 magic;
+	u8 magic2;
+	__le16 len_and_idx;  /* bits[3:0]: idx, bits[15:4]: len */
+} __packed;
+
+#define NCT_HDR_IDX_MASK	0x000f
+#define NCT_HDR_LEN_MASK	0xfff0
+#define NCT_HDR_LEN_SHIFT	4
+
+static inline void nct_set_hdr_idx_len(struct nct_packet_header *hdr,
+				       u8 idx, u16 len)
+{
+	hdr->len_and_idx = cpu_to_le16((len << NCT_HDR_LEN_SHIFT) |
+				       (idx & NCT_HDR_IDX_MASK));
+}
+
+static inline u8 nct_get_hdr_idx(const struct nct_packet_header *hdr)
+{
+	return le16_to_cpu(hdr->len_and_idx) & NCT_HDR_IDX_MASK;
+}
+
+static inline u16 nct_get_hdr_len(const struct nct_packet_header *hdr)
+{
+	return (le16_to_cpu(hdr->len_and_idx) & NCT_HDR_LEN_MASK) >>
+		NCT_HDR_LEN_SHIFT;
+}
+
+/* The definitions are for the fields of nct_ctrl_msg. */
+#define NCT_VCOM_1_STOP_BIT		0
+#define NCT_VCOM_2_STOP_BITS		1
+#define NCT_VCOM_PARITY_NONE		0
+#define NCT_VCOM_PARITY_ODD		1
+#define NCT_VCOM_PARITY_EVEN		2
+#define NCT_VCOM_DL5			0
+#define NCT_VCOM_DL6			1
+#define NCT_VCOM_DL7			2
+#define NCT_VCOM_DL8			3
+#define NCT_VCOM_DISABLE_FLOW_CTRL	0
+#define NCT_VCOM_XOFF			1
+#define NCT_VCOM_RTS_CTS		2
+
+struct nct_ctrl_msg {
+	__le16 val;
+};
+
+#define NCT_CTRL_STOP_BIT_MASK		0x0001
+#define NCT_CTRL_PARITY_MASK		0x0006
+#define NCT_CTRL_PARITY_SHIFT		1
+#define NCT_CTRL_DATA_LEN_MASK		0x0018
+#define NCT_CTRL_DATA_LEN_SHIFT		3
+#define NCT_CTRL_FLOW_MASK		0x0060
+#define NCT_CTRL_FLOW_SHIFT		5
+#define NCT_CTRL_SPD_MASK		0x0f80
+#define NCT_CTRL_SPD_SHIFT		7
+#define NCT_CTRL_RESERVED_MASK		0xf000
+#define NCT_CTRL_RESERVED_SHIFT		12
+
+static inline __le16 nct_build_ctrl_msg(u8 stop_bit, u8 parity, u8 data_len,
+					u8 flow, u8 spd)
+{
+	u16 val = 0;
+
+	val |= (stop_bit & NCT_CTRL_STOP_BIT_MASK);
+	val |= ((parity & (NCT_CTRL_PARITY_MASK >> NCT_CTRL_PARITY_SHIFT)) <<
+		NCT_CTRL_PARITY_SHIFT);
+	val |= ((data_len &
+		 (NCT_CTRL_DATA_LEN_MASK >> NCT_CTRL_DATA_LEN_SHIFT)) <<
+		NCT_CTRL_DATA_LEN_SHIFT);
+	val |= ((flow & (NCT_CTRL_FLOW_MASK >> NCT_CTRL_FLOW_SHIFT)) <<
+		NCT_CTRL_FLOW_SHIFT);
+	val |= ((spd & (NCT_CTRL_SPD_MASK >> NCT_CTRL_SPD_SHIFT)) <<
+		NCT_CTRL_SPD_SHIFT);
+
+	return cpu_to_le16(val);
+}
+
+#define NCT_USR_RDR	0x01
+#define NCT_USR_ORR	0x02
+#define NCT_USR_PBER	0x04
+#define NCT_USR_NSER	0x08
+#define NCT_USR_SBD	0x10
+#define NCT_USR_TBRE	0x20
+#define NCT_USR_TSRE	0x40
+#define NCT_USR_RFEI	0x80
+#define NCT_HSR_TCTS	0x01
+#define NCT_HSR_TDSR	0x02
+#define NCT_HSR_FERI	0x04
+#define NCT_HSR_TDCD	0x08
+#define NCT_HSR_CTS	0x10
+#define NCT_HSR_DSR	0x20
+#define NCT_HSR_RI	0x40
+#define NCT_HSR_DCD	0x80
+#define NCT_HCR_DTR	0x01
+#define NCT_HCR_RTS	0x02
+
+#define NCT_UART_STATE_MSR_MASK (NCT_HSR_TCTS | NCT_HSR_TDSR | \
+				 NCT_HSR_TDCD | NCT_HSR_DCD)
+
+struct nct_port_status {
+	u8 index;
+	u8 usr;
+	u8 hsr;
+	u8 hcr;
+};
+
+struct nct_serial {
+	/* Protects the private data in structure 'nct_serial'. */
+	spinlock_t serial_lock;
+	/* Serializes open/close startup-shutdown transactions. */
+	struct mutex open_close_mutex;
+	unsigned int open_count;
+
+	/* Reading data information */
+	struct nct_tty_port *cur_port;
+	int cur_len;
+
+	bool use_bulk_status;
+	u8 en_device_mask;
+	u8 last_assigned_hw_idx;
+	struct usb_endpoint_descriptor *bulk_out_ep;
+	struct usb_serial_port *port_by_hw_idx[NCT_MAX_NUM_COM_DEVICES];
+};
+
+struct nct_tty_port {
+	u8 hw_idx;
+	u8 usr;
+	u8 hsr;
+	u8 hcr;
+	/*
+	 * Flow control - stop writing data to device.
+	 * 0: Write enable, 1: Stop writing
+	 */
+	bool flow_stop_wrt;
+
+	/* Serializes modem-control transactions sent to the device. */
+	struct mutex mctrl_mutex;
+	spinlock_t port_lock; /* Protects the port data. */
+	bool write_urb_in_use;
+};
+
+/* Functions */
+
+/* Read from USB control pipe */
+static int nct_vendor_read(struct usb_interface *intf,
+			   struct nct_vendor_cmd cmd, void *buf,
+			   int size)
+{
+	struct usb_device *udev = interface_to_usbdev(intf);
+	int ret;
+
+	if (size > NCT_MAX_VENDOR_READ_SIZE)
+		return -EINVAL;
+
+	ret = usb_control_msg_recv(udev, 0,
+				   NCT_VENDOR_COM_READ_REQUEST,
+				   NCT_VENDOR_COM_READ_REQUEST_TYPE,
+				   le16_to_cpu(cmd.val),
+				   intf->cur_altsetting->desc.bInterfaceNumber,
+				   buf, size, 100, GFP_KERNEL);
+
+	return ret;
+}
+
+static int nct_vendor_write(struct usb_interface *intf,
+			    struct nct_vendor_cmd cmd, u16 val)
+{
+	struct device *dev = &intf->dev;
+	struct usb_device *udev = interface_to_usbdev(intf);
+	__le16 le_val = cpu_to_le16(val);
+	int ret;
+
+	ret = usb_control_msg_send(udev, 0,
+				   NCT_VENDOR_COM_WRITE_REQUEST,
+				   NCT_VENDOR_COM_WRITE_REQUEST_TYPE,
+				   le16_to_cpu(cmd.val),
+				   intf->cur_altsetting->desc.bInterfaceNumber,
+				   &le_val, sizeof(le_val), 100, GFP_KERNEL);
+	if (ret)
+		dev_err(dev, "failed to write [%04x]: %d\n",
+			nct_get_cmd_cmd(cmd.val), ret);
+
+	return ret;
+}
+
+/*
+ * Prepare write buffer by extracting data from core write FIFO and
+ * adding header
+ */
+static int nct_prepare_write_buffer(struct usb_serial_port *port,
+				    void *dest, size_t size)
+{
+	unsigned int count;
+	struct nct_packet_header hdr;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+
+	if (size <= sizeof(hdr))
+		return 0;
+
+	/* Leave room for header */
+	count = kfifo_out_locked(&port->write_fifo, dest + sizeof(hdr),
+				 size - sizeof(hdr), &port->lock);
+
+	if (!count)
+		return 0;
+
+	hdr.magic = NCT_HDR_MAGIC;
+	hdr.magic2 = NCT_HDR_MAGIC2;
+	nct_set_hdr_idx_len(&hdr, tport->hw_idx, count);
+
+	/*
+	 * Copy header into buffer (use memcpy to avoid aliasing/unaligned
+	 * access).
+	 */
+	memcpy(dest, &hdr, sizeof(hdr));
+
+	return count + sizeof(hdr);
+}
+
+static u16 nct_set_baud(struct usb_interface *intf, u16 index,
+			unsigned int cflag, bool *found)
+{
+	struct nct_vendor_cmd cmd;
+	struct nct_ctrl_msg msg;
+	u16 i;
+	u8 spd = NCT_DEFAULT_BAUD;
+
+	*found = false;
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_BAUD, index);
+	for (i = 0; i < ARRAY_SIZE(NCT_BAUD_SUP); i++) {
+		if ((cflag & CBAUD) != NCT_BAUD_SUP[i])
+			continue;
+
+		spd = i;
+		/*
+		 * Create control message
+		 * Note: The NCT_VCOM_SET_BAUD only set the baud rate
+		 */
+		msg.val = nct_build_ctrl_msg(0, 0, 0, 0, spd);
+		if (nct_vendor_write(intf, cmd, le16_to_cpu(msg.val)))
+			dev_err(&intf->dev, "set index %d speed error\n",
+				index);
+		else
+			*found = true;
+
+		break;
+	}
+
+	return spd;
+}
+
+static void nct_set_termios(struct tty_struct *tty,
+			    struct usb_serial_port *port,
+			    const struct ktermios *old)
+{
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	struct usb_serial *serial = port->serial;
+	struct usb_interface *intf = serial->interface;
+	struct ktermios *termios = &tty->termios;
+	struct nct_ctrl_msg msg;
+	struct nct_vendor_cmd cmd;
+	u8 stop_bit;
+	u8 parity;
+	u8 data_len;
+	u8 flow;
+	bool baud_found = false;
+	speed_t old_baud;
+
+	/* Device does not support CMSPAR. */
+	termios->c_cflag &= ~CMSPAR;
+
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_CONFIG, tport->hw_idx);
+
+	/* Set stop bit */
+	if (C_CSTOPB(tty))
+		stop_bit = NCT_VCOM_2_STOP_BITS;
+	else
+		stop_bit = NCT_VCOM_1_STOP_BIT;
+
+	/* Set parity */
+	if (C_PARENB(tty)) {
+		if (C_PARODD(tty))
+			parity = NCT_VCOM_PARITY_ODD;
+		else
+			parity = NCT_VCOM_PARITY_EVEN;
+	} else {
+		parity = NCT_VCOM_PARITY_NONE;
+	}
+
+	/* Set data bit length */
+	switch (C_CSIZE(tty)) {
+	case CS5:
+		data_len = NCT_VCOM_DL5;
+		break;
+	case CS6:
+		data_len = NCT_VCOM_DL6;
+		break;
+	case CS7:
+		data_len = NCT_VCOM_DL7;
+		break;
+	case CS8:
+	default:
+		data_len = NCT_VCOM_DL8;
+		break;
+	}
+
+	/* Set flow control */
+	if (C_CRTSCTS(tty))
+		flow = NCT_VCOM_RTS_CTS;
+	else if (I_IXON(tty))
+		flow = NCT_VCOM_XOFF;
+	else
+		flow = NCT_VCOM_DISABLE_FLOW_CTRL;
+
+	tty_port_set_cts_flow(tty->port, C_CRTSCTS(tty));
+
+	/* Create control message (spd=0: speed set separately via SET_BAUD) */
+	msg.val = nct_build_ctrl_msg(stop_bit, parity, data_len, flow, 0);
+	nct_vendor_write(intf, cmd, le16_to_cpu(msg.val));
+
+	/*
+	 * Set baud rate if speed changed.
+	 * Always call tty_encode_baud_rate() to keep kernel's baud rate
+	 * synchronized with the device, especially during initial open
+	 * when the device uses its default baud rate.
+	 */
+	if (!old || old->c_ospeed != termios->c_ospeed)
+		nct_set_baud(intf, tport->hw_idx, termios->c_cflag, &baud_found);
+
+	if (baud_found) {
+		tty_encode_baud_rate(tty, tty_get_baud_rate(tty),
+				     tty_get_baud_rate(tty));
+	} else {
+		if (old && tty_termios_baud_rate(old))
+			old_baud = tty_termios_baud_rate(old);
+		else
+			old_baud = 115200; /* Sync with device default */
+
+		tty_encode_baud_rate(tty, old_baud, old_baud);
+	}
+}
+
+static int nct_break(struct tty_struct *tty, int break_state)
+{
+	struct usb_serial_port *port = tty->driver_data;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	struct usb_serial *serial = port->serial;
+	struct usb_interface *intf = serial->interface;
+	struct nct_vendor_cmd cmd;
+
+	cmd.val = nct_build_cmd(NCT_VCOM_SILENT, tport->hw_idx);
+
+	return nct_vendor_write(intf, cmd, 0);
+}
+
+static int nct_tiocmset_helper(struct tty_struct *tty, unsigned int set,
+			       unsigned int clear)
+{
+	struct usb_serial_port *port = tty->driver_data;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	struct usb_serial *serial = port->serial;
+	struct usb_interface *intf = serial->interface;
+	struct nct_ctrl_msg msg;
+	struct nct_vendor_cmd cmd;
+	u8 hcr;
+	int ret;
+
+	mutex_lock(&tport->mctrl_mutex);
+
+	/*
+	 * port_lock serializes updates of the cached modem-control bits (hcr).
+	 * The control transfer may sleep, so only the hcr read-modify-write is
+	 * done under the spinlock.
+	 */
+	spin_lock_irq(&tport->port_lock);
+	hcr = tport->hcr;
+
+	if (set & TIOCM_RTS)
+		hcr |= NCT_HCR_RTS;
+	if (set & TIOCM_DTR)
+		hcr |= NCT_HCR_DTR;
+	if (clear & TIOCM_RTS)
+		hcr &= ~NCT_HCR_RTS;
+	if (clear & TIOCM_DTR)
+		hcr &= ~NCT_HCR_DTR;
+
+	tport->hcr = hcr;
+	spin_unlock_irq(&tport->port_lock);
+
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_HCR, tport->hw_idx);
+	msg.val = cpu_to_le16(hcr);
+	ret = nct_vendor_write(intf, cmd, le16_to_cpu(msg.val));
+	mutex_unlock(&tport->mctrl_mutex);
+
+	return ret;
+}
+
+static int nct_tiocmget(struct tty_struct *tty)
+{
+	struct usb_serial_port *port = tty->driver_data;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	unsigned int res;
+	u8 hcr, hsr;
+
+	spin_lock_irq(&tport->port_lock);
+	hcr = tport->hcr;
+	hsr = tport->hsr;
+	spin_unlock_irq(&tport->port_lock);
+	res = ((hcr & NCT_HCR_DTR) ? TIOCM_DTR : 0) |
+	      ((hcr & NCT_HCR_RTS) ? TIOCM_RTS : 0) |
+	      ((hsr & NCT_HSR_CTS) ? TIOCM_CTS : 0) |
+	      ((hsr & NCT_HSR_DSR) ? TIOCM_DSR : 0) |
+	      ((hsr & NCT_HSR_RI) ? TIOCM_RI : 0) |
+	      ((hsr & NCT_HSR_DCD) ? TIOCM_CD : 0);
+
+	return res;
+}
+
+static void nct_rx_throttle(struct tty_struct *tty)
+{
+	/* Handle RTS line for RTS/CTS flow control */
+	if (C_CRTSCTS(tty))
+		nct_tiocmset_helper(tty, 0, TIOCM_RTS);
+}
+
+static void nct_rx_unthrottle(struct tty_struct *tty)
+{
+	/* Handle RTS line for RTS/CTS flow control */
+	if (C_CRTSCTS(tty))
+		nct_tiocmset_helper(tty, TIOCM_RTS, 0);
+}
+
+static int nct_serial_write(struct tty_struct *tty,
+			    struct usb_serial_port *port,
+			    const unsigned char *buf,
+			    int count)
+{
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	unsigned int written;
+	int ret;
+
+	/* Flow control */
+	if (tty_port_cts_enabled(tty->port))
+		if (tport->flow_stop_wrt)
+			return 0;
+
+	if (!port)
+		return -ENXIO;
+
+	/* Copy into core write FIFO */
+	written = kfifo_in_locked(&port->write_fifo, buf, count, &port->lock);
+
+	if (written) {
+		ret = usb_serial_generic_write_start(port, GFP_ATOMIC);
+		if (ret && ret != -EBUSY)
+			dev_err(&port->dev,
+				"usb_serial_generic_write_start failed: %d\n",
+				ret);
+	}
+
+	return written;
+}
+
+static unsigned int nct_write_room(struct tty_struct *tty)
+{
+	struct usb_serial_port *port = tty->driver_data;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	unsigned long flags;
+	unsigned int room;
+
+	spin_lock_irqsave(&tport->port_lock, flags);
+	room = kfifo_avail(&port->write_fifo);
+	spin_unlock_irqrestore(&tport->port_lock, flags);
+	return room;
+}
+
+static unsigned int nct_chars_in_buffer(struct tty_struct *tty)
+{
+	struct usb_serial_port *port = tty->driver_data;
+	unsigned int qlen;
+
+	/* Get queued bytes in core write FIFO */
+	qlen = kfifo_len(&port->write_fifo);
+	return qlen;
+}
+/*
+ *  Starts reads urb on all ports. It is to avoid potential issues caused by
+ *  multiple ports being opened almost simultaneously.
+ *  It must be called AFTER startup, with urbs initialized.
+ *  Returns 0 if successful, non-zero error otherwise.
+ */
+static int nct_startup_device(struct usb_serial *serial)
+{
+	int ret = 0;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct usb_serial_port *port;
+	unsigned long flags;
+	bool first_open = false;
+
+	/* Caller must hold serial_priv->open_close_mutex. */
+
+	/* Start URBs on first open */
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	if (serial_priv->open_count++ == 0)
+		first_open = true;
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+	/* Only the first open submits read_urb and, if needed, interrupt_in_urb. */
+	if (!first_open)
+		goto out_return;
+
+	/* Start reading from bulk in endpoint */
+	port = serial->port[0];
+	ret = usb_submit_urb(port->read_urb, GFP_KERNEL);
+	if (ret) {
+		dev_err(&port->dev, "failed to submit read urb: %d\n", ret);
+		goto err_rollback;
+	}
+
+	/* For getting status from interrupt-in */
+	if (!serial_priv->use_bulk_status) {
+		/* Start reading from interrupt pipe */
+		port = serial->port[0];
+		ret = usb_submit_urb(port->interrupt_in_urb, GFP_KERNEL);
+		if (ret) {
+			dev_err(&port->dev,
+				"failed to submit interrupt urb: %d\n",
+				ret);
+			goto err_kill_read;
+		}
+	}
+
+	ret = 0;
+	goto out_return;
+
+err_kill_read:
+	usb_kill_urb(serial->port[0]->read_urb);
+err_rollback:
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	if (serial_priv->open_count)
+		serial_priv->open_count--;
+	if (!serial_priv->open_count) {
+		serial_priv->cur_port = NULL;
+		serial_priv->cur_len = 0;
+	}
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+out_return:
+	return ret;
+}
+
+static void nct_shutdown_device(struct usb_serial *serial)
+{
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	unsigned long flags;
+	bool last_close = false;
+
+	/* Caller must hold serial_priv->open_close_mutex. */
+
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	if (serial_priv->open_count) {
+		serial_priv->open_count--;
+		if (!serial_priv->open_count) {
+			last_close = true;
+			serial_priv->cur_port = NULL;
+			serial_priv->cur_len = 0;
+		}
+	}
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+	if (!last_close)
+		return;
+
+	usb_kill_urb(serial->port[0]->read_urb);
+	if (!serial_priv->use_bulk_status)
+		usb_kill_urb(serial->port[0]->interrupt_in_urb);
+}
+
+static int nct_open(struct tty_struct *tty, struct usb_serial_port *port)
+{
+	struct nct_vendor_cmd cmd;
+	struct nct_ctrl_msg msg;
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	struct usb_serial *serial = port->serial;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct usb_interface *intf = serial->interface;
+	int ret;
+
+	if (!port->serial)
+		return -ENXIO;
+
+	mutex_lock(&serial_priv->open_close_mutex);
+
+	/* Be sure the device is started up */
+	ret = nct_startup_device(port->serial);
+	if (ret)
+		goto out_unlock;
+
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_OPEN_PORT, tport->hw_idx);
+	msg.val = cpu_to_le16(0);
+	ret = nct_vendor_write(intf, cmd, le16_to_cpu(msg.val));
+	if (ret) {
+		dev_err(&port->dev, "Failed to open port: %d\n", ret);
+		nct_shutdown_device(serial);
+		goto out_unlock;
+	}
+
+	mutex_unlock(&serial_priv->open_close_mutex);
+
+	wake_up_interruptible(&port->port.open_wait);
+
+	/*
+	 * Delay 1ms for firmware to configure hardware after opening the port.
+	 * (Especially at high speed)
+	 */
+	usleep_range(1000, 2000);
+	return 0;
+
+out_unlock:
+	mutex_unlock(&serial_priv->open_close_mutex);
+	return ret;
+}
+
+static void nct_close(struct usb_serial_port *port)
+{
+	struct nct_tty_port *tport = usb_get_serial_port_data(port);
+	struct usb_serial *serial = port->serial;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct usb_interface *intf = serial->interface;
+	struct nct_ctrl_msg msg;
+	struct nct_vendor_cmd cmd;
+
+	mutex_lock(&port->serial->disc_mutex);
+	mutex_lock(&serial_priv->open_close_mutex);
+	/* If disconnected, don't send the close-command to the firmware */
+	if (port->serial->disconnected)
+		goto exit;
+
+	/* Send 'Close Port' to the device */
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_CLOSE_PORT, tport->hw_idx);
+	msg.val = cpu_to_le16(0);
+	nct_vendor_write(intf, cmd, le16_to_cpu(msg.val));
+
+exit:
+	nct_shutdown_device(port->serial);
+	mutex_unlock(&serial_priv->open_close_mutex);
+	mutex_unlock(&port->serial->disc_mutex);
+}
+
+static void nct_update_status(struct usb_serial *serial, unsigned char *data)
+{
+	struct nct_port_status *nps = (struct nct_port_status *)data;
+	struct usb_interface *intf = serial->interface;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct nct_tty_port *tport;
+	struct tty_struct *tty;
+	struct usb_serial_port *port;
+	unsigned long flags;
+
+	if (nps->index >= NCT_MAX_NUM_COM_DEVICES) {
+		if (nps->index != NCT_EMPTY_PORT) /* Un-used port */
+			dev_warn(&intf->dev, "receive wrong H/W index\n");
+		return;
+	}
+	if (!(nps->hsr & (NCT_UART_STATE_MSR_MASK | NCT_HSR_CTS)))
+		return; /* No state changes */
+
+	port = serial_priv->port_by_hw_idx[nps->index];
+	if (!port)
+		return;
+
+	tport = usb_get_serial_port_data(port);
+	if (!tport)
+		return;
+
+	spin_lock_irqsave(&tport->port_lock, flags);
+	tport->usr = nps->usr;
+	tport->hsr = nps->hsr;
+	tport->hcr = nps->hcr;
+	spin_unlock_irqrestore(&tport->port_lock, flags);
+
+	if (serial->disconnected) {
+		dev_err(&intf->dev,
+			"device disconnected, skip status update\n");
+		return;
+	}
+
+	tty = tty_port_tty_get(&port->port);
+	if (!tty)
+		return; /* The port has been closed. */
+
+	if (nps->hsr & NCT_UART_STATE_MSR_MASK) {
+		if (nps->hsr & NCT_HSR_DCD) {
+			struct tty_ldisc *ld = tty_ldisc_ref(tty);
+
+			if (ld) {
+				if (ld->ops->dcd_change)
+					ld->ops->dcd_change(tty, 0x01);
+				tty_ldisc_deref(ld);
+			}
+			wake_up_interruptible(&tty->port->open_wait);
+		}
+	}
+
+	/* Flow control */
+	if (tty_port_cts_enabled(&port->port)) {
+		if ((nps->hsr & NCT_HSR_CTS)) {
+			if (tport->flow_stop_wrt)
+				tport->flow_stop_wrt = false;
+		} else {
+			tport->flow_stop_wrt = true;
+		}
+	}
+
+	tty_kref_put(tty);
+}
+
+/*
+ * nct_usb_serial_read - process incoming bulk-in data.
+ *
+ * The device multiplexes all UART ports over a single bulk-in endpoint.
+ * Each data segment is prefixed with a 4-byte header:
+ *
+ *   byte 0: magic (0xa5)
+ *   byte 1: magic2
+ *           0x5a = data packet
+ *           0x5b = status packet
+ *   bytes 2-3: length and index (little-endian 16-bit)
+ *           bits [3:0]  = port index (1-based, valid range 1-7; 0 is reserved)
+ *           bits [15:4] = payload length in bytes
+ *
+ * Multiple packets for different ports may be concatenated within a
+ * single URB. The driver allows a packet's payload to span across URB
+ * boundaries. In that case, the driver records the remaining length (cur_len)
+ * and target port (cur_port) to reassemble the data on subsequent URBs.
+ *
+ * Status packets (magic2 = 0x5b) carry an array of 4-byte per-port
+ * status structures instead of UART data:
+ *
+ *   byte 0: port index (1-based; 0 = reserved, 0xff = unused entry)
+ *   byte 1: UART status (USR)
+ *           bit0: RX data ready
+ *           bit1: overrun error
+ *           bit2: parity error
+ *           bit3: framing error
+ *           bit4: break detected
+ *           bit5: TX buffer empty
+ *           bit6: TX shift register empty
+ *           bit7: RX FIFO error
+ *   byte 2: modem status (MSR)
+ *           bit0: CTS change
+ *           bit1: DSR change
+ *           bit2: RI change
+ *           bit3: DCD change
+ *           bit4: CTS level
+ *           bit5: DSR level
+ *           bit6: RI level
+ *           bit7: DCD level
+ *   byte 3: modem control (MCR)
+ *           bit0: DTR level
+ *           bit1: RTS level
+ */
+static void nct_usb_serial_read(struct urb *urb)
+{
+	struct usb_serial_port *port = urb->context;
+	struct usb_serial *serial = port->serial;
+	struct usb_interface *intf = serial->interface;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct nct_tty_port *tport;
+	struct nct_packet_header *hdr = NULL;
+	unsigned char *data = urb->transfer_buffer;
+	int i, j;
+	int actual_len = urb->actual_length;
+	int len = 0;
+	u8 hdr_idx;
+	struct nct_port_status *nps;
+	unsigned long flags;
+
+	if (!urb->actual_length)
+		return;
+
+again:
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	tport = serial_priv->cur_port;
+	if (!tport) {
+		/*
+		 * Handle a new data packet (i.e., it is not
+		 * the remaining data without a header).
+		 * The packet does not need reassembly at this point.
+		 */
+		for (i = 0; i < urb->actual_length; i++) {
+			if (i + sizeof(struct nct_packet_header) >
+			    urb->actual_length) {
+				/* Not enough data for a header */
+				spin_unlock_irqrestore(&serial_priv->serial_lock,
+						       flags);
+				return;
+			}
+
+			hdr = (struct nct_packet_header *)data;
+			/* Decode the header */
+
+			/*
+			 * Status data is also transmitted via bulk-in
+			 * pipe.
+			 * Current FW reports a fixed 24-byte status payload.
+			 */
+			if (serial_priv->use_bulk_status &&
+			    hdr->magic == NCT_HDR_MAGIC &&
+			    hdr->magic2 == NCT_HDR_MAGIC_STATUS &&
+			    nct_get_hdr_len(hdr) == 24 &&
+			    actual_len >= 28) {
+				/*
+				 * Notice: actual_len will be decreased,
+				 * it is equal to urb->actual_length
+				 * only at the beginning.
+				 */
+
+				/*
+				 * Status report.
+				 * It should be a standalone package in
+				 * one URB.
+				 */
+				data += sizeof(struct nct_packet_header);
+				actual_len -= sizeof(struct nct_packet_header);
+
+				nps = (struct nct_port_status *)data;
+				if ((actual_len % sizeof(*nps)) != 0) {
+					dev_err(&intf->dev,
+						"bad status len: %d\n",
+						actual_len);
+					spin_unlock_irqrestore(
+						&serial_priv->serial_lock,
+						flags);
+					return;
+				}
+
+				for (j = 0; j < (actual_len / sizeof(*nps));
+				     j++) {
+					nct_update_status(serial,
+							  (unsigned char *)nps);
+					nps++;
+				}
+
+				spin_unlock_irqrestore(&serial_priv->serial_lock,
+						       flags);
+				return;
+			}
+
+			hdr_idx = nct_get_hdr_idx(hdr);
+			if (hdr->magic == NCT_HDR_MAGIC &&
+			    hdr->magic2 == NCT_HDR_MAGIC2 &&
+			    hdr_idx > 0 &&
+			    hdr_idx < NCT_MAX_NUM_COM_DEVICES &&
+			    nct_get_hdr_len(hdr) <= 512)
+				break;
+
+			data++;
+			actual_len--;
+			if (!actual_len) {
+				dev_err(&intf->dev,
+					"decode serial packet size failed\n");
+				spin_unlock_irqrestore(&serial_priv->serial_lock,
+						       flags);
+				return;
+			}
+		}
+		/*
+		 * Resolve TTY port by the idx in header and ensure it is valid.
+		 * Record it for two purposes:
+		 *   (1) If the current packet payload is incomplete, the next URB
+		 *       continues payload bytes without a new header.
+		 *   (2) Route received bytes to the correct TTY port.
+		 */
+		port = serial_priv->port_by_hw_idx[nct_get_hdr_idx(hdr)];
+		if (!port) {
+			dev_err(&intf->dev,
+				"decode serial packet index failed\n");
+			spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+			return;
+		}
+
+		tport = usb_get_serial_port_data(port);
+		if (!tport) {
+			spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+			return;
+		}
+		/*
+		 * Calculate the data length.
+		 * Then, check if the length specified in the header matches
+		 * the data length. If not, it indicates that the data we
+		 * received spans across two (or more) packets.
+		 */
+		actual_len -= sizeof(struct nct_packet_header);
+		data += sizeof(struct nct_packet_header);
+		/* actual_len: the data length of the data we got this time */
+		if (nct_get_hdr_len(hdr) > actual_len) {
+			/*
+			 * It means the length specified in the header (the
+			 * custom header) is greater than the length of the
+			 * data we received.
+			 * Therefore, the data received this time belongs to a
+			 * single packet payload segment (i.e. no next header in this URB).
+			 */
+			len = actual_len;
+			/*
+			 * cur_len: Record how many data does not handle yet
+			 */
+			serial_priv->cur_len = nct_get_hdr_len(hdr) - len;
+			/*
+			 * Record the current port. When we receive the
+			 * remaining data for this packet next time
+			 */
+			serial_priv->cur_port = tport;
+		} else {
+			/*
+			 * The received data crosses packet boundaries (does not belong
+			 * to the same header). We only handle data by
+			 * the length in header. And we will handle
+			 * another packet when 'goto "again" '.
+			 */
+			len = nct_get_hdr_len(hdr);
+		}
+	} else { /* Handling remaining data that crosses packet boundaries */
+		if (serial_priv->cur_len > actual_len) {
+			/*
+			 * The unhandled part of the data exceeds the data we
+			 * received this time. We only handle the data we
+			 * have, expecting more data to be received later.
+			 */
+			len = actual_len;
+		} else {
+			/*
+			 * This means the packet has been fully handled.
+			 * Clear 'cur_port' as no additional data needs to be
+			 * attached to the current packet.
+			 */
+			len = serial_priv->cur_len;
+			serial_priv->cur_port = NULL;
+		}
+		serial_priv->cur_len -= len;
+	}
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+	tty_insert_flip_string(&port->port, data, len);
+	data += len;
+	/*
+	 * Send data to the tty device (according to the port identified above).
+	 */
+	tty_flip_buffer_push(&port->port);
+	actual_len -= len;
+
+	/*
+	 * It means that the data we received this time contains two or
+	 * more data packets, so it needs to continue processing the next
+	 * data packets.
+	 */
+	if (actual_len > 0)
+		goto again;
+}
+
+static void nct_process_read_bulk(struct urb *urb)
+{
+	struct usb_serial_port *port =  urb->context;
+	struct nct_serial *serial_priv = usb_get_serial_data(port->serial);
+	bool stopped = false;
+	int status = urb->status;
+	int ret;
+
+	switch (status) {
+	case 0:
+		nct_usb_serial_read(urb);
+		break;
+	case -ENOENT:
+	case -ECONNRESET:
+	case -ESHUTDOWN:
+		if (!(status == -ENOENT
+		      && serial_priv
+		      && serial_priv->open_count
+		      && !port->serial->disconnected)) {
+			dev_dbg(&port->dev, "urb halted, status=%d\n", status);
+			stopped = true;
+		}
+		break;
+	case -EPIPE:
+		dev_err(&port->dev, "urb stalled (EPIPE)\n");
+		stopped = true;
+		break;
+	default:
+		dev_dbg(&port->dev, "nonzero urb status: %d\n", status);
+		break;
+	}
+
+	if (stopped)
+		return;
+
+	ret = usb_submit_urb(urb, GFP_ATOMIC);
+	if (ret != 0 && ret != -EPERM)
+		dev_err(&port->dev,
+			"failed resubmitting urb, ret=%d\n",
+			ret);
+}
+
+static void nct_read_bulk_callback(struct urb *urb)
+{
+	struct usb_serial_port *port = urb->context;
+
+	/* Do not resubmit if the urb is being killed */
+	if (urb->status == -ESHUTDOWN)
+		return;
+
+	if (!port || !port->serial)
+		return;
+
+	if (port->serial->disconnected)
+		return;
+
+	/* Processing data */
+	nct_process_read_bulk(urb);
+}
+
+static int nct_calc_num_ports(struct usb_serial *serial,
+			      struct usb_serial_endpoints *epds)
+{
+	struct nct_vendor_cmd cmd;
+	u8 buf[8];
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct usb_interface *intf = serial->interface;
+	struct usb_host_interface *iface_desc = intf->cur_altsetting;
+	struct usb_endpoint_descriptor *bulk_in = NULL;
+	struct usb_endpoint_descriptor *bulk_out = NULL;
+	struct usb_endpoint_descriptor *int_in = NULL;
+	int ret;
+	int i;
+	int num_ports;
+
+	/* Send init command */
+	cmd.val = nct_build_cmd(NCT_VCOM_SET_INIT, NCT_VCOM_INDEX_GLOBAL);
+	ret = nct_vendor_write(intf, cmd, 0);
+	if (ret) {
+		dev_err(&intf->dev, "set COM init error\n");
+		return ret;
+	}
+
+	/* Get ports' index supported by the device(/FW) */
+	cmd.val = nct_build_cmd(NCT_VCOM_GET_PORTS_SUPPORT, NCT_VCOM_INDEX_GLOBAL);
+	ret = nct_vendor_read(intf, cmd, buf, 1);
+	if (ret) {
+		dev_err(&intf->dev,
+			"get COM port index error\n");
+		return ret;
+	}
+
+	if (!serial_priv) {
+		serial_priv = kzalloc_obj(*serial_priv, GFP_KERNEL);
+		if (!serial_priv)
+			return -ENOMEM;
+		spin_lock_init(&serial_priv->serial_lock);
+		mutex_init(&serial_priv->open_close_mutex);
+		usb_set_serial_data(serial, serial_priv);
+	}
+	serial_priv->en_device_mask = buf[0];
+	serial_priv->last_assigned_hw_idx = 0; /* Note: hw_idx is based on 1 */
+
+	for (i = 0, num_ports = 0; i < NCT_MAX_NUM_COM_DEVICES; i++) {
+		if (!(buf[0] & BIT(i)))
+			continue;	/* The port is disabled */
+
+		num_ports++;
+	}
+
+	for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
+		struct usb_endpoint_descriptor *endpoint =
+			&iface_desc->endpoint[i].desc;
+
+		if (!bulk_in && usb_endpoint_is_bulk_in(endpoint))
+			bulk_in = endpoint;
+		if (!bulk_out && usb_endpoint_is_bulk_out(endpoint))
+			bulk_out = endpoint;
+		if (!int_in && usb_endpoint_is_int_in(endpoint))
+			int_in = endpoint;
+	}
+
+	if (!bulk_in || !bulk_out)
+		return -ENODEV;
+
+	for (i = 0; i < num_ports; i++) {
+		epds->bulk_in[i] = bulk_in;
+		epds->bulk_out[i] = bulk_out;
+		if (int_in && i == 0)
+			epds->interrupt_in[i] = int_in;
+	}
+	epds->num_bulk_out = num_ports;
+
+	return num_ports;
+}
+
+static int nct_attach(struct usb_serial *serial)
+{
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+
+	if (!serial_priv)
+		return -ENODEV;
+
+	serial_priv->use_bulk_status = true;
+	if (serial->port[0] && serial->port[0]->interrupt_in_urb)
+		serial_priv->use_bulk_status = false;
+
+	return 0;
+}
+
+static int nct_port_probe(struct usb_serial_port *port)
+{
+	struct nct_tty_port *tport;
+	struct usb_serial *serial = port->serial;
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	unsigned long flags;
+	int i, ret;
+	unsigned int fifo_state;
+
+	tport = kzalloc_obj(*tport, GFP_KERNEL);
+	if (!tport)
+		return -ENOMEM;
+
+	/* Assign hw_idx */
+	mutex_init(&tport->mctrl_mutex);
+	spin_lock_init(&tport->port_lock);
+
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	for (i = serial_priv->last_assigned_hw_idx + 1;
+	     i < NCT_MAX_NUM_COM_DEVICES; i++) {
+		if ((serial_priv->en_device_mask & (1 << i)) == 0)
+			continue; /* The port is disabled */
+
+		tport->hw_idx = i;
+		serial_priv->last_assigned_hw_idx = i;
+		break;
+	}
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+	usb_set_serial_port_data(port, tport);
+
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	serial_priv->port_by_hw_idx[tport->hw_idx] = port;
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+	fifo_state = kfifo_initialized(&port->write_fifo);
+
+	/*
+	 * Keep the FIFO if it is already initialized.
+	 */
+	if (fifo_state)
+		return 0;
+	ret = kfifo_alloc(&port->write_fifo, NCT_WRITE_FIFO_SIZE, GFP_KERNEL);
+	if (ret) {
+		kfree(tport);
+		return ret;
+	}
+
+	return 0;
+}
+
+static void nct_interrupt_in_callback(struct urb *urb)
+{
+	struct usb_serial_port *port = urb->context;
+	int status = urb->status;
+	struct usb_serial *serial = port->serial;
+	unsigned char *data = urb->transfer_buffer;
+	int retval;
+	int i;
+	int actual_len = urb->actual_length;
+	struct nct_port_status *nps;
+
+	switch (status) {
+	case 0:
+		/* Success */
+		if ((actual_len % sizeof(*nps)) != 0)
+			return;
+
+		nps = (struct nct_port_status *)data;
+		for (i = 0; i < (actual_len / sizeof(*nps)); i++) {
+			nct_update_status(serial, (unsigned char *)nps);
+			nps++;
+		}
+		break;
+
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		/* This urb is terminated, clean up */
+		return;
+	default:
+		break;
+	}
+	retval = usb_submit_urb(urb, GFP_ATOMIC);
+	if (retval)
+		dev_err(&port->dev, "submit intr URB failed, ret=%d\n",
+			retval);
+}
+
+static void nct_disconnect(struct usb_serial *serial)
+{
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	unsigned long flags;
+
+	/* Reset status */
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	serial_priv->open_count = 0;
+	serial_priv->cur_port = NULL;
+	serial_priv->cur_len = 0;
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+}
+
+static void nct_port_remove(struct usb_serial_port *port)
+{
+	struct nct_tty_port *priv = usb_get_serial_port_data(port);
+	struct nct_serial *serial_priv = usb_get_serial_data(port->serial);
+	unsigned long flags;
+
+	if (priv) {
+		spin_lock_irqsave(&serial_priv->serial_lock, flags);
+		if (priv->hw_idx < NCT_MAX_NUM_COM_DEVICES)
+			serial_priv->port_by_hw_idx[priv->hw_idx] = NULL;
+		spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+	}
+	kfree(priv);
+}
+
+static void nct_release(struct usb_serial *serial)
+{
+	struct nct_serial *serial_priv;
+
+	serial_priv = usb_get_serial_data(serial);
+	kfree(serial_priv);
+}
+
+static int nct_suspend(struct usb_serial *serial, pm_message_t message)
+{
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	int i;
+
+	/* Stop all URBs */
+	usb_kill_urb(serial->port[0]->read_urb);
+	for (i = 0; i < serial->type->num_ports; i++)
+		usb_kill_urb(serial->port[i]->write_urb);
+
+	if (!serial_priv->use_bulk_status)
+		usb_kill_urb(serial->port[0]->interrupt_in_urb);
+
+	return 0;
+}
+
+static int nct_resume(struct usb_serial *serial)
+{
+	struct nct_serial *serial_priv = usb_get_serial_data(serial);
+	struct usb_interface *intf = serial->interface;
+	unsigned long flags;
+	int ret;
+
+	/* Reset driver internal state */
+	spin_lock_irqsave(&serial_priv->serial_lock, flags);
+	serial_priv->cur_port = NULL;
+	serial_priv->cur_len = 0;
+	spin_unlock_irqrestore(&serial_priv->serial_lock, flags);
+
+	/* Resubmit URBs */
+	if (!serial_priv->open_count)
+		return 0;
+
+	ret = usb_submit_urb(serial->port[0]->read_urb, GFP_KERNEL);
+	if (ret)
+		dev_err(&intf->dev, "submit read URB failed, ret=%d\n",
+			ret);
+
+	if (!serial_priv->use_bulk_status) {
+		ret = usb_submit_urb(serial->port[0]->interrupt_in_urb,
+				     GFP_KERNEL);
+		if (ret)
+			dev_err(&intf->dev,
+				"submit interrupt URB failed, ret=%d\n",
+				ret);
+	}
+
+	return 0;
+}
+
+static struct usb_serial_driver nct_usb_serial_device = {
+	.driver	= {
+		.name =		"nct_mtuart",
+	},
+	.description =		"Nuvoton USB to serial adapter",
+	.id_table =		id_table,
+	.num_bulk_in =		1,
+	.num_bulk_out =		1,
+	.open =			nct_open,
+	.close =		nct_close,
+	.write =		nct_serial_write,
+	.write_room =		nct_write_room,
+	.write_bulk_callback =	usb_serial_generic_write_bulk_callback,
+	.prepare_write_buffer =	nct_prepare_write_buffer,
+	.read_bulk_callback =	nct_read_bulk_callback,
+	.read_int_callback =	nct_interrupt_in_callback,
+	.chars_in_buffer =	nct_chars_in_buffer,
+	.throttle =		nct_rx_throttle,
+	.unthrottle =		nct_rx_unthrottle,
+	.attach =		nct_attach,
+	.calc_num_ports =	nct_calc_num_ports,
+	.set_termios =		nct_set_termios,
+	.break_ctl =		nct_break,
+	.tiocmget =		nct_tiocmget,
+	.tiocmset =		nct_tiocmset_helper,
+	.disconnect =		nct_disconnect,
+	.release =		nct_release,
+	.port_probe =		nct_port_probe,
+	.port_remove =		nct_port_remove,
+	.suspend =		nct_suspend,
+	.resume =		nct_resume,
+};
+
+static struct usb_serial_driver * const nct_serial_drivers[] = {
+	&nct_usb_serial_device, NULL
+};
+
+module_usb_serial_driver(nct_serial_drivers, id_table);
+MODULE_DESCRIPTION("Nuvoton USB to serial adaptor driver");
+MODULE_AUTHOR("Sheng-Yuan Huang <syhuang3@nuvoton.com>");
+MODULE_LICENSE("GPL v2");
+
-- 
2.43.0
Re: [PATCH v6] USB: serial: nct_usb_serial: add support for Nuvoton USB adapter
Posted by Greg Kroah-Hartman 2 weeks, 6 days ago
On Tue, Mar 17, 2026 at 02:14:46PM +0800, hsyemail2@gmail.com wrote:
> --- /dev/null
> +++ b/drivers/usb/serial/nct_usb_serial.c
> @@ -0,0 +1,1455 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (C) 2024-2025 Nuvoton Corp
> + * Copyright (C) 2024-2025 Sheng-Yuan Huang <syhuang3@nuvoton.com>

It is now 2026 :)