Re: [PATCH] usb-serial: support NAL Research Corporation Iridium modems
From: Johan Hovold
Date: Fri Nov 13 2020 - 08:17:18 EST
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