Thread (6 messages) 6 messages, 3 authors, 10d ago

Re: [PATCH net-next v7 1/2] net: wwan: core: propagate modem control signals to port drivers

flat view

From: Loic Poulain <loic.poulain@oss.qualcomm.com>
Date: 2026-10-06 08:30:47
Also in: linux-arm-msm, lkml

Hi Peter,

On Tue, Oct 6, 2026 at 6:05 AM Peter Hunt [off-list ref] wrote:
quoted hunk ↗ jump to hunk
The WWAN character device emulates the TTY modem-control ioctls
(TIOCMGET/TIOCMSET/TIOCMBIC/TIOCMBIS) for AT and QCDM ports, but the
result is only stored in port->at_data.mdmbits and never reaches the port
driver. A driver therefore cannot act on the host raising or dropping
DTR/RTS, even though some modems depend on it (e.g. they withhold
unsolicited AT result codes until the host asserts DTR).

Add an optional ->dtr_rts(port, mdmbits) operation to struct wwan_port_ops.
Drivers that implement it receive the full TIOCM bitmask so they can assert
or de-assert DTR and RTS independently. The wwan core tracks the full TIOCM
bitmask in port->at_data.mdmbits and calls ->dtr_rts when it changes, gated
on WWAN_PORT_AT to match the open/close raise/drop behaviour.

Also raise DTR/RTS in wwan_port_op_start on first open of an AT port when
the driver implements ->dtr_rts, and drop them in wwan_port_op_stop on
last close if HUPCL is set in the port's termios. This mirrors TTY
semantics and means individual drivers do not need to implement this
themselves. As on a TTY, HUPCL defaults to on for AT ports, and userspace
can clear it with TCSETS to keep DTR asserted across close, for example so
that closing the port does not end a call on a modem set to AT&D1/AT&D2.

at_data.mdmbits is protected by data_lock. In the ioctl path the ->dtr_rts
call is deferred until ops_lock is held, where mdmbits is re-read under
data_lock, so the value passed to the driver always reflects the committed
bitmask under ops_lock and is serialised against concurrent ioctls and
against port removal (which nulls port->ops under ops_lock).

wwan_remove_port() also drops DTR/RTS before ->stop() when a port is
removed while still open, regardless of HUPCL since the device is going
away. Both drop paths share a helper and pass the driver the resulting
bitmask.

Signed-off-by: Peter Hunt <redacted>
---
v7: Drop DTR/RTS on last close only if HUPCL is set, and default HUPCL on
    for AT ports. Pass the masked mdmbits rather than 0 from
    wwan_remove_port(), via a helper shared with the close path. Both
    from the Sashiko review of v6
v6: Rebase onto net-next. Snapshot mdmbits under data_lock before calling
    ->dtr_rts from wwan_port_op_start()/wwan_port_op_stop(), rather than
    reading it after data_lock has been released
v5: Change ->dtr_rts signature from bool to unsigned int mdmbits so DTR
    and RTS can be driven independently; re-read mdmbits inside ops_lock
    in the ioctl path to close a concurrent-ioctl ordering race; add
    de-assert call to wwan_remove_port() for the hot-unplug case; update
    kernel-doc to note the op is AT-only and describe the mdmbits argument
v4: Protect at_data.mdmbits in wwan_port_op_start/stop under data_lock;
    release data_lock and acquire ops_lock with a NULL check before calling
    ->dtr_rts from the ioctl path; gate ioctl ->dtr_rts on WWAN_PORT_AT to
    match open/close behaviour; reduce boolean to TIOCM_DTR only
v3: Replace ->tiocmget/->tiocmset with ->dtr_rts(port, bool on) modelled
    on tty_port_operations.dtr_rts; raise/drop DTR/RTS in
    wwan_port_op_start/stop rather than in the driver (Loic Poulain)
---
 drivers/net/wwan/wwan_core.c | 61 +++++++++++++++++++++++++++++++++++-
 include/linux/wwan.h         |  3 ++
 2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wwan/wwan_core.c b/drivers/net/wwan/wwan_core.c
index ffbcf11e4e68..eba4b0582b5a 100644
--- a/drivers/net/wwan/wwan_core.c
+++ b/drivers/net/wwan/wwan_core.c
@@ -655,6 +655,10 @@ struct wwan_port *wwan_create_port(struct device *parent,
        init_waitqueue_head(&port->waitqueue);
        mutex_init(&port->data_lock);

+       /* AT ports hang up on last close by default, as a TTY does */
+       if (type == WWAN_PORT_AT)
+               port->at_data.termios.c_cflag = HUPCL;
+
        port->dev.parent = &wwandev->dev;
        port->dev.type = &wwan_port_dev_type;
        dev_set_drvdata(&port->dev, drvdata);
@@ -679,12 +683,34 @@ struct wwan_port *wwan_create_port(struct device *parent,
 }
 EXPORT_SYMBOL_GPL(wwan_create_port);

+/* Drop DTR/RTS on an AT port. Called with ops_lock held. On last close
+ * the lines are only dropped if HUPCL is set, as for a TTY, on port
+ * removal they are always dropped.
+ */
+static void wwan_port_drop_dtr_rts(struct wwan_port *port, bool hupcl_only)
+{
+       unsigned int bits;
+
+       mutex_lock(&port->data_lock);
+       if (hupcl_only && !(port->at_data.termios.c_cflag & HUPCL)) {
+               mutex_unlock(&port->data_lock);
Using (scoped) guard version would be simpler:
scoped_guard(mutex, &port->data_lock) {
quoted hunk ↗ jump to hunk
+               return;
+       }
+       port->at_data.mdmbits &= ~(TIOCM_DTR | TIOCM_RTS);
+       bits = port->at_data.mdmbits;
+       mutex_unlock(&port->data_lock);
+
+       port->ops->dtr_rts(port, bits);
+}
+
 void wwan_remove_port(struct wwan_port *port)
 {
        struct wwan_device *wwandev = to_wwan_dev(port->dev.parent);

        mutex_lock(&port->ops_lock);
        if (port->start_count) {
+               if (port->type == WWAN_PORT_AT && port->ops->dtr_rts)
I would recommend checking for the dtr_rts callback in
wwan_port_drop_dtr_rts() rather than here.
quoted hunk ↗ jump to hunk
+                       wwan_port_drop_dtr_rts(port, false);
                port->ops->stop(port);
                port->start_count = 0;
        }
@@ -759,8 +785,20 @@ static int wwan_port_op_start(struct wwan_port *port)
        if (!port->start_count)
                ret = port->ops->start(port);

-       if (!ret)
+       if (!ret) {
                port->start_count++;
+               /* Mirror TTY semantics: raise DTR/RTS on first open of an AT port */
+               if (port->start_count == 1 && port->type == WWAN_PORT_AT &&
+                   port->ops->dtr_rts) {
+                       unsigned int bits;
+
+                       mutex_lock(&port->data_lock);
+                       port->at_data.mdmbits |= TIOCM_DTR | TIOCM_RTS;
+                       bits = port->at_data.mdmbits;
+                       mutex_unlock(&port->data_lock);
+                       port->ops->dtr_rts(port, bits);
Could we add a wwan_port_raise_dtr_rts() counterpart to
wwan_port_drop_dtr_rts() and move this AT/TTY-specific handling there?
quoted hunk ↗ jump to hunk
+               }
+       }

 out_unlock:
        mutex_unlock(&port->ops_lock);
@@ -773,6 +811,11 @@ static void wwan_port_op_stop(struct wwan_port *port)
        mutex_lock(&port->ops_lock);
        port->start_count--;
        if (!port->start_count) {
+               /* Mirror TTY semantics: drop DTR/RTS on last close of an AT
+                * port if HUPCL is set
+                */
+               if (port->ops && port->type == WWAN_PORT_AT && port->ops->dtr_rts)
+                       wwan_port_drop_dtr_rts(port, true);
                if (port->ops)
                        port->ops->stop(port);
                skb_queue_purge(&port->rxq);
@@ -980,6 +1023,7 @@ static long wwan_port_fops_at_ioctl(struct wwan_port *port, unsigned int cmd,
                                    unsigned long arg)
 {
        int ret = 0;
+       bool call_dtr_rts = false;

        mutex_lock(&port->data_lock);
@@ -1036,6 +1080,8 @@ static long wwan_port_fops_at_ioctl(struct wwan_port *port, unsigned int cmd,
                        port->at_data.mdmbits |= mdmbits;
                else
                        port->at_data.mdmbits = mdmbits;
+               if (port->type == WWAN_PORT_AT)
+                       call_dtr_rts = true;
                break;
        }
@@ -1061,6 +1107,19 @@ static long wwan_port_fops_at_ioctl(struct wwan_port *port, unsigned int cmd,

        mutex_unlock(&port->data_lock);

+       if (call_dtr_rts) {
+               unsigned int bits;
+
+               mutex_lock(&port->ops_lock);
+               if (port->ops && port->ops->dtr_rts) {
+                       mutex_lock(&port->data_lock);
+                       bits = port->at_data.mdmbits;
+                       mutex_unlock(&port->data_lock);
+                       port->ops->dtr_rts(port, bits);
+               }
+               mutex_unlock(&port->ops_lock);
+       }
+
        return ret;
 }
diff --git a/include/linux/wwan.h b/include/linux/wwan.h
index 1e0e2cb53579..57406139304e 100644
--- a/include/linux/wwan.h
+++ b/include/linux/wwan.h
@@ -57,6 +57,8 @@ struct wwan_port;
  * @tx_blocking: Optional blocking routine that sends WWAN port protocol data
  *               to the device.
  * @tx_poll: Optional routine that sets additional TX poll flags.
+ * @dtr_rts: Optional routine that updates the modem control lines to match
+ *           @mdmbits (a TIOCM_* bitmask). Only called for WWAN_PORT_AT ports.
  *
  * The wwan_port_ops structure contains a list of low-level operations
  * that control a WWAN port device. All functions are mandatory unless specified.
@@ -70,6 +72,7 @@ struct wwan_port_ops {
        int (*tx_blocking)(struct wwan_port *port, struct sk_buff *skb);
        __poll_t (*tx_poll)(struct wwan_port *port, struct file *filp,
                            poll_table *wait);
+       void (*dtr_rts)(struct wwan_port *port, unsigned int mdmbits);
 };

 /** struct wwan_port_caps - The WWAN port capbilities
--
2.43.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help