Re: [PATCH v2] tty/serial: Add a serial port simulator

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Tue, Mar 05, 2019 at 11:12:31AM -0600, minyard@xxxxxxx wrote:
> From: Corey Minyard <cminyard@xxxxxxxxxx>
> 
> This creates simulated serial ports, both as echo devices and pipe
> devices.  The driver reasonably approximates the serial port speed
> and simulates some modem control lines.  It allows error injection
> and direct control of modem control lines.

I like this, thanks for doing it!

Minor nits below:

> +You can modify null modem and control the lines individually
> +through an interface in /sys/class/tty/ttyECHO<n>/ctrl,
> +/sys/class/tty/ttyPipeA<n>/ctrl, and
> +/sys/class/tty/ttyPipeB<n>/ctrl.  The following may be written to
> +those files:
> +
> +[+-]nullmodem
> +    enable/disable null modem
> +
> +[+-]cd
> +    enable/disable Carrier Detect (no effect if +nullmodem)
> +
> +[+-]dsr
> +    enable/disable Data Set Ready (no effect if +nullmodem)
> +
> +[+-]cts
> +    enable/disable Clear To Send (no effect if +nullmodem)
> +
> +[+-]ring
> +    enable/disable Ring
> +
> +frame
> +    inject a frame error on the next byte
> +
> +parity
> +    inject a parity error on the next byte
> +
> +overrun
> +    inject an overrun error on the next byte
> +
> +The contents of the above files has the following format:
> +
> +tty[Echo|PipeA|PipeB]<n>
> +    <mctrl values>
> +
> +where <mctrl values> is the modem control values above (not frame,
> +parity, or overrun) with the following added:
> +
> +[+-]dtr
> +    value of the Data Terminal Ready
> +
> +[+-]rts
> +    value of the Request To Send
> +
> +The above values are not settable through this interface, they are
> +set through the serial port interface itself.
> +
> +So, for instance, ttyEcho0 comes up in the following state::
> +
> +   # cat /sys/class/tty/ttyEcho0/ctrl
> +   ttyEcho0: +nullmodem -cd -dsr -cts -ring -dtr -rts
> +
> +If something connects, it will become::
> +
> +   ttyEcho0: +nullmodem +cd +dsr +cts -ring +dtr +rts
> +
> +To enable ring::
> +
> +   # echo "+ring" >/sys/class/tty/ttyEcho0/ctrl
> +   # cat /sys/class/tty/ttyEcho0/ctrl
> +   ttyEcho0: +nullmodem +cd +dsr +cts +ring +dtr +rts
> +
> +Now disable NULL modem and the CD line::
> +
> +   # echo "-nullmodem -cd" >/sys/class/tty/ttyEcho0/ctrl
> +   # cat /sys/class/tty/ttyEcho0/ctrl
> +   ttyEcho0: -nullmodem -cd -dsr -cts +ring -dtr -rts
> +
> +Note that these settings are for the side you are modifying.  So if
> +you set nullmodem on ttyPipeA0, that controls whether the DTR/RTS
> +lines from ttyPipeB0 affect ttyPipeA0.  It doesn't affect ttyPipeB's
> +modem control lines.

All of the sysfs stuff needs to also go in Documentation/ABI to describe
your new files.

> +config SERIAL_SIMULATOR
> +	tristate "Serial port simulator"
> +	default n

n is the default, no need to say it again here.

> --- /dev/null
> +++ b/drivers/tty/serial/serialsim.c
> @@ -0,0 +1,1101 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +
> +/*
> + * serialsim - Emulate a serial device in a loopback and/or pipe
> + *
> + * See the docs for this for more details.

Pointer to the docs?

And no copyright notice?  I don't object to it, but it is usually nice
to see.

> + *
> + */
> +
> +#include <linux/module.h>
> +#include <linux/moduleparam.h>
> +#include <linux/ioport.h>
> +#include <linux/init.h>
> +#include <linux/serial.h>
> +#include <linux/tty.h>
> +#include <linux/tty_flip.h>
> +#include <linux/device.h>
> +#include <linux/serial_core.h>
> +#include <linux/kthread.h>
> +#include <linux/hrtimer.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/ctype.h>
> +#include <linux/string.h>
> +#include <linux/uaccess.h>
> +
> +#include <linux/serialsim.h>
> +
> +#define PORT_SERIALECHO 72549
> +#define PORT_SERIALPIPEA 72550
> +#define PORT_SERIALPIPEB 72551

tabs?

> +
> +#ifdef CONFIG_HIGH_RES_TIMERS
> +#define SERIALSIM_WAKES_PER_SEC	1000
> +#else
> +#define SERIALSIM_WAKES_PER_SEC	HZ
> +#endif
> +
> +#define SERIALSIM_XBUFSIZE	32
> +
> +/* For things to send on the line, in flags field. */
> +#define DO_FRAME_ERR		(1 << TTY_FRAME)
> +#define DO_PARITY_ERR		(1 << TTY_PARITY)
> +#define DO_OVERRUN_ERR		(1 << TTY_OVERRUN)
> +#define DO_BREAK		(1 << TTY_BREAK)
> +#define FLAGS_MASK (DO_FRAME_ERR | DO_PARITY_ERR | DO_OVERRUN_ERR | DO_BREAK)
> +
> +struct serialsim_intf {
> +	struct uart_port port;
> +
> +	/*
> +	 * This is my transmit buffer, my thread picks this up and
> +	 * injects them into the other port's uart.
> +	 */
> +	unsigned char xmitbuf[SERIALSIM_XBUFSIZE];
> +	struct circ_buf buf;
> +
> +	/* Error flags to send. */
> +	bool break_reported;
> +	unsigned int flags;
> +
> +	/* Modem state. */
> +	unsigned int mctrl;
> +	bool do_null_modem;
> +	spinlock_t mctrl_lock;
> +	struct tasklet_struct mctrl_tasklet;
> +
> +	/* My transmitter is enabled. */
> +	bool tx_enabled;
> +
> +	/* I can receive characters. */
> +	bool rx_enabled;
> +
> +	/* Is the port registered with the uart driver? */
> +	bool registered;
> +
> +	/*
> +	 * The serial echo port on the other side of this pipe (or points
> +	 * to myself in loopback mode.
> +	 */
> +	struct serialsim_intf *ointf;
> +
> +	unsigned int div;
> +	unsigned int bytes_per_interval;
> +	unsigned int per_interval_residual;
> +
> +	struct ktermios termios;
> +
> +	const char *threadname;
> +	struct task_struct *thread;
> +
> +	struct serial_rs485 rs485;
> +};
> +
> +#define circ_sbuf_space(buf) CIRC_SPACE((buf)->head, (buf)->tail, \
> +					SERIALSIM_XBUFSIZE)
> +#define circ_sbuf_empty(buf) ((buf)->head == (buf)->tail)
> +#define circ_sbuf_next(idx) (((idx) + 1) % SERIALSIM_XBUFSIZE)

Don't we have a ring buffer (or 3) structure already?  Did you create
another one?

> +static void serialsim_thread_delay(void)
> +{
> +#ifdef CONFIG_HIGH_RES_TIMERS
> +	ktime_t timeout;
> +
> +	timeout = ns_to_ktime(1000000000 / SERIALSIM_WAKES_PER_SEC);
> +	set_current_state(TASK_INTERRUPTIBLE);
> +	schedule_hrtimeout(&timeout, HRTIMER_MODE_REL);
> +#else
> +	schedule_timeout_interruptible(1);
> +#endif
> +}

Why do you care about hires timers here?  Why not always just sleep to
slow things down?

> +
> +static int serialsim_thread(void *data)
> +{
> +	struct serialsim_intf *intf = data;
> +	struct serialsim_intf *ointf = intf->ointf;
> +	struct uart_port *port = &intf->port;
> +	struct uart_port *oport = &ointf->port;
> +	struct circ_buf *tbuf = &intf->buf;
> +	unsigned int residual = 0;
> +
> +	while (!kthread_should_stop()) {

Aren't we trying to get away from creating new kthreads?

Can you just use a workqueue entry?

> +static unsigned int nr_echo_ports = 4;
> +module_param(nr_echo_ports, uint, 0444);
> +MODULE_PARM_DESC(nr_echo_ports,
> +		 "The number of echo ports to create.  Defaults to 4");
> +
> +static unsigned int nr_pipe_ports = 4;
> +module_param(nr_pipe_ports, uint, 0444);
> +MODULE_PARM_DESC(nr_pipe_ports,
> +		 "The number of pipe ports to create.  Defaults to 4");

No way to just do this with configfs and not worry about module params?

> +static char *gettok(char **s)
> +{
> +	char *t = skip_spaces(*s);
> +	char *p = t;
> +
> +	while (*p && !isspace(*p))
> +		p++;
> +	if (*p)
> +		*p++ = '\0';
> +	*s = p;
> +
> +	return t;
> +}

We don't have this already in the tree?

> +static bool tokeq(const char *t, const char *m)
> +{
> +	return strcmp(t, m) == 0;
> +}

same here.

> +
> +static unsigned int parse_modem_line(char op, unsigned int flag,
> +				     unsigned int *mctrl)
> +{
> +	if (op == '+')
> +		*mctrl |= flag;
> +	else
> +		*mctrl &= ~flag;
> +	return flag;
> +}
> +
> +static void serialsim_ctrl_append(char **val, int *left, char *n, bool enabled)
> +{
> +	int count;
> +
> +	count = snprintf(*val, *left, " %c%s", enabled ? '+' : '-', n);
> +	*left -= count;
> +	*val += count;
> +}

sysfs files really should only be "one value per file", so this api is
troubling :(

> +static ssize_t serialsim_ctrl_read(struct device *dev,
> +				   struct device_attribute *attr,
> +				   char *buf)
> +{
> +	struct tty_port *tport = dev_get_drvdata(dev);
> +	struct uart_state *state = container_of(tport, struct uart_state, port);
> +	struct uart_port *port = state->uart_port;
> +	struct serialsim_intf *intf = serialsim_port_to_intf(port);
> +	unsigned int mctrl = intf->mctrl;
> +	char *val = buf;
> +	int left = PAGE_SIZE;
> +	int count;
> +
> +	count = snprintf(val, left, "%s:", dev->kobj.name);

dev_name()?

And is it really needed?  It's already known as this is the sysfs file
for that device.  Don't make userspace parse it away.

> +	val += count;
> +	left -= count;
> +	serialsim_ctrl_append(&val, &left, "nullmodem", intf->do_null_modem);
> +	serialsim_ctrl_append(&val, &left, "cd", mctrl & TIOCM_CAR);
> +	serialsim_ctrl_append(&val, &left, "dsr", mctrl & TIOCM_DSR);
> +	serialsim_ctrl_append(&val, &left, "cts", mctrl & TIOCM_CTS);
> +	serialsim_ctrl_append(&val, &left, "ring", mctrl & TIOCM_RNG);
> +	serialsim_ctrl_append(&val, &left, "dtr", mctrl & TIOCM_DTR);
> +	serialsim_ctrl_append(&val, &left, "rts", mctrl & TIOCM_RTS);
> +	*val++ = '\n';
> +
> +	return val - buf;
> +}
> +
> +static ssize_t serialsim_ctrl_write(struct device *dev,
> +				    struct device_attribute *attr,
> +				    const char *val, size_t count)
> +{
> +	struct tty_port *tport = dev_get_drvdata(dev);
> +	struct uart_state *state = container_of(tport, struct uart_state, port);
> +	struct uart_port *port = state->uart_port;
> +	struct serialsim_intf *intf = serialsim_port_to_intf(port);
> +	char *str = kstrndup(val, count, GFP_KERNEL);
> +	char *p, *s = str;
> +	int rv = count;
> +	unsigned int flags = 0;
> +	unsigned int nullmodem = 0;
> +	unsigned int mctrl_mask = 0, mctrl = 0;
> +
> +	if (!str)
> +		return -ENOMEM;
> +
> +	p = gettok(&s);
> +	while (*p) {
> +		char op = '\0';
> +		int err = 0;
> +
> +		switch (*p) {
> +		case '+':
> +		case '-':
> +			op = *p++;
> +			break;
> +		default:
> +			break;
> +		}
> +
> +		if (tokeq(p, "frame"))
> +			flags |= DO_FRAME_ERR;
> +		else if (tokeq(p, "parity"))
> +			flags |= DO_PARITY_ERR;
> +		else if (tokeq(p, "overrun"))
> +			flags |= DO_OVERRUN_ERR;
> +		else if (tokeq(p, "nullmodem"))
> +			nullmodem = op;
> +		else if (tokeq(p, "dsr"))
> +			mctrl_mask |= parse_modem_line(op, TIOCM_DSR, &mctrl);
> +		else if (tokeq(p, "cts"))
> +			mctrl_mask |= parse_modem_line(op, TIOCM_CTS, &mctrl);
> +		else if (tokeq(p, "cd"))
> +			mctrl_mask |= parse_modem_line(op, TIOCM_CAR, &mctrl);
> +		else if (tokeq(p, "ring"))
> +			mctrl_mask |= parse_modem_line(op, TIOCM_RNG, &mctrl);
> +		else
> +			err = -EINVAL;
> +
> +		if (err) {
> +			rv = err;
> +			goto out;
> +		}
> +		p = gettok(&s);
> +	}
> +
> +	if (flags)
> +		serialsim_set_flags(intf, flags);
> +	if (nullmodem)
> +		serialsim_set_null_modem(intf, nullmodem == '+');
> +	if (mctrl_mask)
> +		serialsim_set_modem_lines(intf, mctrl_mask, mctrl);
> +
> +out:
> +	kfree(str);
> +
> +	return rv;
> +}

This worries me, parsing sysfs files is ripe for problems.  configfs
might be better here.

> +
> +static DEVICE_ATTR(ctrl, 0660, serialsim_ctrl_read, serialsim_ctrl_write);

DEVICE_ATTR_RW()?

> +
> +static struct attribute *serialsim_dev_attrs[] = {
> +	&dev_attr_ctrl.attr,
> +	NULL,
> +};
> +
> +static struct attribute_group serialsim_dev_attr_group = {
> +	.attrs = serialsim_dev_attrs,
> +};

ATTRIBUTE_GROUPS()?

> +static int __init serialsim_init(void)
> +{
> +	unsigned int i;
> +	int rv;
> +
> +	serialecho_ports = kcalloc(nr_echo_ports,
> +				   sizeof(*serialecho_ports),
> +				   GFP_KERNEL);
> +	if (!serialecho_ports) {
> +		pr_err("serialsim: Unable to allocate echo ports.\n");

No need to print error for memory failure.

> +		rv = ENOMEM;

-ENOMEM?

> +		goto out;
> +	}
> +
> +	serialpipe_ports = kcalloc(nr_pipe_ports * 2,
> +				   sizeof(*serialpipe_ports),
> +				   GFP_KERNEL);
> +	if (!serialpipe_ports) {
> +		kfree(serialecho_ports);
> +		pr_err("serialsim: Unable to allocate pipe ports.\n");

Same here.

> +		rv = ENOMEM;

-ENOMEM?

> +		goto out;

Shouldn't out clean up stuff?

> +	}
> +
> +	serialecho_driver.nr = nr_echo_ports;
> +	rv = uart_register_driver(&serialecho_driver);
> +	if (rv) {
> +		kfree(serialecho_ports);
> +		kfree(serialpipe_ports);
> +		pr_err("serialsim: Unable to register driver.\n");
> +		goto out;

Again, out should clean up stuff for an error.  Will make the code below
simpler.

> +	}
> +
> +	serialpipea_driver.nr = nr_pipe_ports;
> +	rv = uart_register_driver(&serialpipea_driver);
> +	if (rv) {
> +		uart_unregister_driver(&serialecho_driver);
> +		kfree(serialecho_ports);
> +		kfree(serialpipe_ports);
> +		pr_err("serialsim: Unable to register driver.\n");
> +		goto out;
> +	}
> +
> +	serialpipeb_driver.nr = nr_pipe_ports;
> +	rv = uart_register_driver(&serialpipeb_driver);
> +	if (rv) {
> +		uart_unregister_driver(&serialpipea_driver);
> +		uart_unregister_driver(&serialecho_driver);
> +		kfree(serialecho_ports);
> +		kfree(serialpipe_ports);
> +		pr_err("serialsim: Unable to register driver.\n");
> +		goto out;
> +	}
> +
> +	for (i = 0; i < nr_echo_ports; i++) {
> +		struct serialsim_intf *intf = &serialecho_ports[i];
> +		struct uart_port *port = &intf->port;
> +
> +		intf->buf.buf = intf->xmitbuf;
> +		intf->ointf = intf;
> +		intf->threadname = "serialecho";
> +		intf->do_null_modem = true;
> +		spin_lock_init(&intf->mctrl_lock);
> +		tasklet_init(&intf->mctrl_tasklet, mctrl_tasklet, (long) intf);
> +		/* Won't configure without some I/O or mem address set. */
> +		port->iobase = 1;
> +		port->line = i;
> +		port->flags = UPF_BOOT_AUTOCONF;
> +		port->ops = &serialecho_ops;
> +		spin_lock_init(&port->lock);
> +		port->attr_group = &serialsim_dev_attr_group;
> +		rv = uart_add_one_port(&serialecho_driver, port);
> +		if (rv)
> +			pr_err("serialsim: Unable to add uart port %d: %d\n",
> +			       i, rv);
> +		else
> +			intf->registered = true;
> +	}
> +
> +	for (i = 0; i < nr_pipe_ports * 2; i += 2) {
> +		struct serialsim_intf *intfa = &serialpipe_ports[i];
> +		struct serialsim_intf *intfb = &serialpipe_ports[i + 1];
> +		struct uart_port *porta = &intfa->port;
> +		struct uart_port *portb = &intfb->port;
> +
> +		intfa->buf.buf = intfa->xmitbuf;
> +		intfb->buf.buf = intfb->xmitbuf;
> +		intfa->ointf = intfb;
> +		intfb->ointf = intfa;
> +		intfa->threadname = "serialpipea";
> +		intfb->threadname = "serialpipeb";
> +		spin_lock_init(&intfa->mctrl_lock);
> +		spin_lock_init(&intfb->mctrl_lock);
> +		tasklet_init(&intfa->mctrl_tasklet, mctrl_tasklet,
> +			     (long) intfa);
> +		tasklet_init(&intfb->mctrl_tasklet, mctrl_tasklet,
> +			     (long) intfb);
> +
> +		/* Won't configure without some I/O or mem address set. */
> +		porta->iobase = 1;
> +		porta->line = i / 2;
> +		porta->flags = UPF_BOOT_AUTOCONF;
> +		porta->ops = &serialpipea_ops;
> +		spin_lock_init(&porta->lock);
> +		porta->attr_group = &serialsim_dev_attr_group;
> +		porta->rs485_config = serialsim_rs485;
> +
> +		/*
> +		 * uart_add_one_port() does an mctrl operation, which will
> +		 * claim the other port's lock.  So everything needs to be
> +		 * full initialized, and we need null modem off until we
> +		 * get things added.
> +		 */
> +		portb->iobase = 1;
> +		portb->line = i / 2;
> +		portb->flags = UPF_BOOT_AUTOCONF;
> +		portb->ops = &serialpipeb_ops;
> +		portb->attr_group = &serialsim_dev_attr_group;
> +		spin_lock_init(&portb->lock);
> +		portb->rs485_config = serialsim_rs485;
> +
> +		rv = uart_add_one_port(&serialpipea_driver, porta);
> +		if (rv) {
> +			pr_err("serialsim: Unable to add uart pipe a port %d: %d\n",
> +			       i, rv);
> +			continue;
> +		} else {
> +			intfa->registered = true;
> +		}
> +
> +		rv = uart_add_one_port(&serialpipeb_driver, portb);
> +		if (rv) {
> +			pr_err("serialsim: Unable to add uart pipe b port %d: %d\n",
> +			       i, rv);
> +			intfa->registered = false;
> +			uart_remove_one_port(&serialpipea_driver, porta);
> +		} else {
> +			intfb->registered = true;
> +		}
> +
> +		if (intfa->registered && intfb->registered) {
> +			serialsim_set_null_modem(intfa, true);
> +			serialsim_set_null_modem(intfb, true);
> +		}
> +	}
> +	rv = 0;
> +
> +	pr_info("serialsim ready\n");

Don't be noisy for when all is good.  Drivers should be quiet.

> --- /dev/null
> +++ b/include/uapi/linux/serialsim.h
> @@ -0,0 +1,32 @@
> +/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
> +
> +/*
> + * serialsim - Emulate a serial device in a loopback and/or pipe
> + */
> +
> +/*
> + * TTY IOCTLs for controlling the modem control and for error injection.
> + * See drivers/tty/serial/serialsim.c and Documentation/serial/serialsim.rst
> + * for details.
> + */
> +
> +#ifndef LINUX_SERIALSIM_H
> +#define LINUX_SERIALSIM_H
> +
> +#include <linux/ioctl.h>
> +#include <asm/termbits.h>
> +
> +#define TIOCSERSREMNULLMODEM	0x54e4
> +#define TIOCSERSREMMCTRL	0x54e5
> +#define TIOCSERSREMERR		0x54e6
> +#ifdef TCGETS2
> +#define TIOCSERGREMTERMIOS	_IOR('T', 0xe7, struct termios2)
> +#else
> +#define TIOCSERGREMTERMIOS	_IOR('T', 0xe7, struct termios)
> +#endif
> +#define TIOCSERGREMNULLMODEM	_IOR('T', 0xe8, int)
> +#define TIOCSERGREMMCTRL	_IOR('T', 0xe9, unsigned int)
> +#define TIOCSERGREMERR		_IOR('T', 0xea, unsigned int)
> +#define TIOCSERGREMRS485	_IOR('T', 0xeb, struct serial_rs485)

Wait, don't we have ioctls for these things in the tty layer already?
WHy add new ones?

thanks,

greg k-h




[Index of Archives]     [Kernel Newbies]     [Security]     [Netfilter]     [Bugtraq]     [Linux PPP]     [Linux FS]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Samba]     [Video 4 Linux]     [Linmodem]     [Device Mapper]     [Linux Kernel for ARM]

  Powered by Linux