Re: [PATCH] usb-serial: support NAL Research Corporation Iridium modems

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

 



On Tue, Oct 20, 2020 at 05:54:26PM +0200, Bertold Van den Bergh wrote:
> This driver is for NAL Reserach Corporation Iridium modems with USB.

"Research"

> There are different variants of the bus protocol, but the driver should
> detect this automatically.

Do you mean that this is something to be implemented?

> Signed-off-by: Bertold Van den Bergh <vandenbergh@xxxxxxxxxxx>
> ---
>  Documentation/usb/usb-serial.rst |  15 ++
>  drivers/usb/serial/Kconfig       |   9 +
>  drivers/usb/serial/Makefile      |   1 +
>  drivers/usb/serial/nal.c         | 357 +++++++++++++++++++++++++++++++
>  4 files changed, 382 insertions(+)
>  create mode 100644 drivers/usb/serial/nal.c
> 
> diff --git a/Documentation/usb/usb-serial.rst b/Documentation/usb/usb-serial.rst
> index 8fa7dbd3d..fdc8c0c76 100644
> --- a/Documentation/usb/usb-serial.rst
> +++ b/Documentation/usb/usb-serial.rst
> @@ -494,6 +494,21 @@ Moschip MCS7720, MCS7715 driver
>        with this driver with a simple addition to the usb_device_id table.  I
>        don't have one of these devices, so I can't say for sure.
>  
> +NAL Research Corporation driver
> +-------------------------------
> +
> +  This driver is for NAL Reserach Corporation Iridium modems with USB.

Same typo.

> +  There are different variants of the bus protocol, but the driver should
> +  detect this automatically.
> +
> +  The manufacturer provided Windows driver attaches to all PIDs in the given
> +  VID, so you may want to try this driver even if the PID doesn't match.
> +
> +  The manufacturer's website: https://www.nalresearch.com/
> +
> +  For any questions or problems with this driver, please contact:
> +  Bertold Van den Bergh <vandenbergh@xxxxxxxxxxx>
> +
>  Generic Serial driver
>  ---------------------
>  
> diff --git a/drivers/usb/serial/Kconfig b/drivers/usb/serial/Kconfig
> index 4007fa25a..f97c44068 100644
> --- a/drivers/usb/serial/Kconfig
> +++ b/drivers/usb/serial/Kconfig
> @@ -436,6 +436,15 @@ config USB_SERIAL_MXUPORT
>  	  To compile this driver as a module, choose M here: the
>  	  module will be called mxuport.
>  
> +config USB_SERIAL_NAL
> +	tristate "USB NAL Research Corporation Serial Bridge"
> +	help
> +	  Say Y here if you want to use NAL Research Corporation
> +	  USB devices.
> +
> +	  To compile this driver as a module, choose M here: the
> +	  module will be called nal.
> +
>  config USB_SERIAL_NAVMAN
>  	tristate "USB Navman GPS device"
>  	help
> diff --git a/drivers/usb/serial/Makefile b/drivers/usb/serial/Makefile
> index 2d491e434..f3cbe972f 100644
> --- a/drivers/usb/serial/Makefile
> +++ b/drivers/usb/serial/Makefile
> @@ -40,6 +40,7 @@ obj-$(CONFIG_USB_SERIAL_METRO)			+= metro-usb.o
>  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_NAL)			+= nal.o
>  obj-$(CONFIG_USB_SERIAL_NAVMAN)			+= navman.o
>  obj-$(CONFIG_USB_SERIAL_OMNINET)		+= omninet.o
>  obj-$(CONFIG_USB_SERIAL_OPTICON)		+= opticon.o
> diff --git a/drivers/usb/serial/nal.c b/drivers/usb/serial/nal.c
> new file mode 100644
> index 000000000..e3272cd5e
> --- /dev/null
> +++ b/drivers/usb/serial/nal.c
> @@ -0,0 +1,357 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Driver for NAL Research Corporation USB serial converter.
> + * Tested using A3LA-XG.
> + *
> + * Copyright (C) 2020 Bertold Van den Bergh (vandenbergh@xxxxxxxxxxx)
> + *

Please provide some details on the protocol here. What are the header
types, that you use a bulk pipe for modem control, etc.

> + */
> +
> +#include <linux/kernel.h>
> +#include <linux/errno.h>
> +#include <linux/slab.h>
> +#include <linux/tty.h>
> +#include <linux/tty_flip.h>
> +#include <linux/module.h>
> +#include <linux/usb.h>
> +#include <linux/usb/serial.h>
> +#include <linux/mutex.h>
> +#include <linux/spinlock.h>
> +#include <linux/workqueue.h>
> +
> +static const struct usb_device_id id_table[] = {
> +	{ USB_DEVICE(0x2017, 0x0001) },
> +	{ }
> +};
> +
> +MODULE_DEVICE_TABLE(usb, id_table);
> +
> +struct nal_serial_private {
> +	struct usb_device       *dev;
> +	struct mutex             cmd_mutex; //Mutex for sharing cmd_buf

Please don't use // comments and shorten to something like

	/* protects cmd_buf */

> +	unsigned char            cmd_buf[64];
> +
> +	spinlock_t               lock;      //Lock for sharing next three entries

Here too.

> +	unsigned char            control_get;
> +	unsigned char            control_put;
> +	unsigned char            header_type;
> +
> +	wait_queue_head_t        control_event;
> +
> +	struct workqueue_struct *work_queue;
> +	struct work_struct       control_work;
> +	struct work_struct       data_work;
> +};
> +
> +static int nal_prepare_write_buffer(struct usb_serial_port *port,
> +				    void *buf, size_t count);
> +static void nal_process_read_urb(struct urb *urb);
> +static int nal_request(struct nal_serial_private *priv, int type);
> +static void nal_data_work(struct work_struct *work);
> +static int nal_tiocmget(struct tty_struct *tty);
> +static int nal_send_control(struct nal_serial_private *priv,
> +			    unsigned int set, unsigned int clear);
> +static int nal_tiocmset(struct tty_struct *tty,
> +			unsigned int set, unsigned int clear);
> +static void nal_dtr_rts(struct usb_serial_port *port, int on);
> +static void nal_control_work(struct work_struct *work);
> +static int nal_port_probe(struct usb_serial_port *serial);
> +static int nal_port_remove(struct usb_serial_port *serial);
> +
> +static struct usb_serial_driver nal_device = {
> +	.driver = {
> +		.owner =	THIS_MODULE,
> +		.name =		"nal",
> +	},
> +	.id_table		= id_table,
> +	.num_ports		= 1,
> +	.tiocmget		= nal_tiocmget,
> +	.tiocmset		= nal_tiocmset,
> +	.port_probe		= nal_port_probe,
> +	.port_remove		= nal_port_remove,
> +	.dtr_rts		= nal_dtr_rts,
> +	.process_read_urb       = nal_process_read_urb,
> +	.prepare_write_buffer   = nal_prepare_write_buffer
> +};
> +
> +static struct usb_serial_driver * const serial_drivers[] = {
> +	&nal_device, NULL
> +};

Move the driver structures to the end of the file and drop the function
prototypes (may need to reorder some functions).

> +
> +#define CONTROL_DSR (1)
> +#define CONTROL_CD  (2)
> +#define CONTROL_RI  (4)
> +#define CONTROL_CTS (8)
> +#define CONTROL_DTR (16)
> +#define CONTROL_RTS (32)

No need for parentheses, use BIT(n).

> +
> +static int nal_prepare_write_buffer(struct usb_serial_port *port,
> +				    void *buf, size_t count)
> +{
> +	struct nal_serial_private *priv = usb_get_serial_port_data(port);
> +	unsigned char *header = (unsigned char *)buf;

Cast not needed.

> +	unsigned char header_type, cout;
> +
> +	spin_lock(&priv->lock);

You need to disable interrupts also in the completion handlers (which
may run in soft-interrupt context); use spin_lock_irqsave().

> +	header_type = priv->header_type;
> +	spin_unlock(&priv->lock);
> +
> +	count = min_t(size_t, count, 64 - header_type);

Please clarify how the max-packet size correlates with the header types
by using a helper function (and defines for the types).

> +
> +	cout = kfifo_out_locked(&port->write_fifo, buf + header_type,
> +				count, &port->lock) + header_type;

Is header type really a header length?

> +
> +	header[0] = 5;

What's 5 here? Use a defines for magic constants throughout.

> +
> +	if (!header_type) {
> +		return 0;

You're throwing away data here.

> +	} else if (header_type == 2) {
> +		header[1] = count;
> +		return 64;

What if there wasn't 64 bytes in the fifo? Should the stale buffer data
be cleared?

> +	}
> +
> +	return cout;
> +}
> +
> +static void nal_process_read_urb(struct urb *urb)
> +{
> +	struct usb_serial_port *port = urb->context;
> +	struct nal_serial_private *priv = usb_get_serial_port_data(port);
> +	const unsigned char *buf = (const unsigned char *)urb->transfer_buffer;

Cast not needed.

> +	unsigned char length;
> +
> +	if (urb->actual_length < 1)
> +		return;
> +
> +	if (!priv->header_type && urb->actual_length < 64) {
> +		spin_lock(&priv->lock);
> +		priv->header_type = 1;
> +		spin_unlock(&priv->lock);
> +	}

Can you do the header type (protocol?) detection at probe somehow
instead? Send a request and read it back synchronously?

> +
> +	if (priv->header_type != 1)
> +		schedule_work(&priv->data_work);

What's going on here? Looks like you're polling for data, but that
should be made clear here (use a helper function, or at least add a
comment).

How does it depend on the header type?

> +
> +	if (buf[0] == 5 && urb->actual_length >= 2) {
> +		if (!priv->header_type) {
> +			return;

Throwing away data if you haven't yet detected the protocol?

> +		} else if (priv->header_type == 1) {
> +			tty_insert_flip_string(&port->port, buf + 1,
> +					       urb->actual_length - 1);
> +		} else {
> +			length = buf[1];
> +			if (length > urb->actual_length - 2)
> +				length = urb->actual_length - 2;
> +
> +			tty_insert_flip_string(&port->port, buf + 2, length);
> +		}
> +
> +		tty_flip_buffer_push(&port->port);
> +	} else if (buf[0] == 0 && urb->actual_length >= 2) {
> +		spin_lock(&priv->lock);
> +		priv->control_get = 0x80 | buf[1];

What's going on here? What is command/reply "0"? Use a define.

What is 0x80? Use a define.

> +		if (!priv->header_type && urb->actual_length == 64)
> +			priv->header_type = 2;
> +		spin_unlock(&priv->lock);
> +
> +		wake_up(&priv->control_event);
> +	} else if (buf[0] == 1) {

What's "1"? You get the point...

> +		schedule_work(&priv->control_work);

And why do you need to set the control lines on a device request here?

> +	} else {
> +		dev_info(&priv->dev->dev, "Unsupported input (length=%u): %02x\n",
> +			 urb->actual_length, buf[0]);

Use &port->dev for messages, and this one should be demoted to
dev_dbg().

> +	}
> +}
> +
> +static int nal_request(struct nal_serial_private *priv, int type)
> +{
> +	int ret_val;

Please use the shorter "ret" throughout.

> +
> +	mutex_lock(&priv->cmd_mutex);
> +
> +	/* type==0: Request control lines
> +	 * type==1: Request application data
> +	 */

Use an enum or defines for the request types.

Comment style should be

	/*
	 * ...
	 */

> +	priv->cmd_buf[0] = type ? 0x04 : 0x01;
> +	priv->cmd_buf[1] = 0xFF;

Defines.

> +
> +	ret_val = usb_bulk_msg(priv->dev, usb_sndbulkpipe(priv->dev, 1),
> +			       priv->cmd_buf, 64, NULL, HZ);

Timeout is specified in milliseconds and should not depend on HZ.

Note that cmd_buf must be allocated separately from priv as it may be
mapped for DMA.

> +
> +	mutex_unlock(&priv->cmd_mutex);
> +
> +	return ret_val;
> +}
> +
> +static void nal_data_work(struct work_struct *work)
> +{
> +	struct nal_serial_private *priv =
> +		container_of(work, struct nal_serial_private, data_work);
> +
> +	nal_request(priv, 1);
> +}
> +
> +static int nal_tiocmget(struct tty_struct *tty)
> +{
> +	struct usb_serial_port *port = tty->driver_data;
> +	struct nal_serial_private *priv = usb_get_serial_port_data(port);
> +	int ret_val, control = 0;
> +
> +	spin_lock(&priv->lock);

You must disable interrupt when taking this lock (throughout).

> +	priv->control_get = 0;
> +	spin_unlock(&priv->lock);
> +
> +	ret_val = nal_request(priv, 0);
> +	if (ret_val)
> +		return ret_val;
> +
> +	while (!control) {

do-while

> +		ret_val = wait_event_interruptible_timeout(priv->control_event,
> +							   priv->control_get > 0, HZ);

Looks like this could be simplified by using a struct completion and
calling wait_for_completion_interruptible_timeout() here instead. Just
store the latest modem control status in the completion handler and call
complete().

> +		if (ret_val == 0)
> +			ret_val = -ETIMEDOUT;
> +		if (ret_val < 0)
> +			return ret_val;
> +
> +		spin_lock(&priv->lock);
> +		control = priv->control_get;
> +		spin_unlock(&priv->lock);
> +	}
> +
> +	ret_val = ((control & CONTROL_DSR) ? TIOCM_DSR : 0) |
> +		 ((control & CONTROL_CD)  ? TIOCM_CD  : 0) |
> +		 ((control & CONTROL_RI)  ? TIOCM_RI  : 0) |
> +		 ((control & CONTROL_CTS) ? TIOCM_CTS : 0);
> +
> +	spin_lock(&priv->lock);
> +	control = priv->control_put;
> +	spin_unlock(&priv->lock);

Are DTR and RTS included in the bulk response you get? If so you may not
need to store control_put at all. Just return the latest values returned
by the device.

> +
> +	ret_val |= ((control & CONTROL_DTR) ? TIOCM_DTR : 0) |
> +		  ((control & CONTROL_RTS) ? TIOCM_RTS : 0);
> +
> +	return ret_val;
> +}
> +
> +static int nal_send_control(struct nal_serial_private *priv,
> +			    unsigned int set, unsigned int clear)
> +{
> +	int ret_val, control;
> +
> +	mutex_lock(&priv->cmd_mutex);
> +
> +	spin_lock(&priv->lock);
> +	if (set & TIOCM_RTS)
> +		priv->control_put |= CONTROL_RTS;
> +	if (set & TIOCM_DTR)
> +		priv->control_put |= CONTROL_DTR;
> +	if (clear & TIOCM_RTS)
> +		priv->control_put &= ~CONTROL_RTS;
> +	if (clear & TIOCM_DTR)
> +		priv->control_put &= ~CONTROL_DTR;
> +
> +	control = priv->control_put;
> +	spin_unlock(&priv->lock);
> +
> +	priv->cmd_buf[0] = 0x00;
> +	priv->cmd_buf[1] = 0x0d | control;
> +
> +	ret_val = usb_bulk_msg(priv->dev, usb_sndbulkpipe(priv->dev, 1),
> +			       priv->cmd_buf, 64, NULL, HZ);
> +
> +	mutex_unlock(&priv->cmd_mutex);
> +
> +	return ret_val;
> +}
> +
> +static int nal_tiocmset(struct tty_struct *tty,
> +			unsigned int set, unsigned int clear)
> +{
> +	struct usb_serial_port *port = tty->driver_data;
> +	struct nal_serial_private *priv = usb_get_serial_port_data(port);
> +
> +	return nal_send_control(priv, set, clear);
> +}
> +
> +static void nal_dtr_rts(struct usb_serial_port *port, int enable)
> +{
> +	struct nal_serial_private *priv = usb_get_serial_port_data(port);
> +
> +	if (enable)
> +		nal_send_control(priv, TIOCM_RTS | TIOCM_DTR, 0);
> +	else
> +		nal_send_control(priv, 0, TIOCM_RTS | TIOCM_DTR);
> +}
> +
> +static void nal_control_work(struct work_struct *work)
> +{
> +	struct nal_serial_private *priv =
> +		container_of(work, struct nal_serial_private, control_work);
> +
> +	nal_send_control(priv, 0, 0);
> +}
> +
> +static int nal_port_probe(struct usb_serial_port *serial)
> +{
> +	struct nal_serial_private *priv;
> +	int ret_val;
> +
> +	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	priv->dev = serial->serial->dev;
> +	spin_lock_init(&priv->lock);
> +	init_waitqueue_head(&priv->control_event);
> +	mutex_init(&priv->cmd_mutex);
> +
> +	priv->work_queue = alloc_workqueue("nal_wq", 0, 0);
> +	if (!priv->work_queue) {
> +		ret_val = -ENOMEM;
> +		goto fail_queue;
> +	}

You never use the work queue so remove it.

> +
> +	usb_set_serial_port_data(serial, priv);
> +
> +	INIT_WORK(&priv->control_work, nal_control_work);
> +	INIT_WORK(&priv->data_work, nal_data_work);
> +
> +	/* Used for header autodetect */
> +	ret_val = nal_request(priv, 0);

Again, what is 0...

So this is used to trigger a reply that you read when the port is
opened?

> +	if (ret_val < 0)
> +		goto fail_probe;
> +
> +	ret_val = nal_send_control(priv, TIOCM_RTS | TIOCM_DTR, 0);
> +	if (ret_val < 0)
> +		goto fail_probe;

This is taken care of by the dtr_rts callback on open().

> +
> +	return 0;
> +
> +fail_probe:
> +	cancel_work_sync(&priv->data_work);
> +	cancel_work_sync(&priv->control_work);

These cannot possibly be scheduled yet, right? Can you cancel (or flush)
them on close instead?

> +	destroy_workqueue(priv->work_queue);
> +fail_queue:
> +	kfree(priv);
> +	return ret_val;

Name labels after what they do rather than where you jump from (e.g.
"err_cancel_work" and "err_free").

> +}
> +
> +static int nal_port_remove(struct usb_serial_port *serial)
> +{
> +	struct nal_serial_private *priv = usb_get_serial_port_data(serial);
> +
> +	cancel_work_sync(&priv->data_work);
> +	cancel_work_sync(&priv->control_work);
> +	destroy_workqueue(priv->work_queue);
> +	kfree(priv);
> +
> +	return 0;
> +}
> +
> +module_usb_serial_driver(serial_drivers, id_table);
> +
> +#define AUTHOR "Bertold Van den Bergh <vandenbergh@xxxxxxxxxxx>"
> +#define DESC   "Driver for NAL Research Corporation USB serial interface"

No need for these, just use the module macros directly.

> +MODULE_AUTHOR(AUTHOR);
> +MODULE_DESCRIPTION(DESC);
> +MODULE_LICENSE("GPL v2");

The shorter "GPL" means the same here.

Ok, I think I get the protocol now, but it really shouldn't take that
much effort. You need to go through the driver and add helper functions
and/or aptly named defines so that the protocol becomes more or less
obvious to someone looking at this code. And please add a short overview
of the protocol to the file header.

Johan



[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux