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

From: Loic Poulain

Date: Tue Oct 06 2026 - 04:30:33 EST


Hi Peter,

On Tue, Oct 6, 2026 at 6:05 AM Peter Hunt <peter.hunt@xxxxxxxxxxxx> wrote:
>
> 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 <peter.hunt@xxxxxxxxxxxx>
> ---
> 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) {

> + 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.

> + 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?

> + }
> + }
>
> 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
>