Netdev List
 help / color / mirror / Atom feed
* [PATCH net-next v2 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL
@ 2026-08-06 15:52 Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Peter Hunt @ 2026-08-06 15:52 UTC (permalink / raw)
  To: mani, loic.poulain, ryazanov.s.a
  Cc: johannes, netdev, mhi, linux-arm-msm, linux-kernel, Peter Hunt

The WWAN character device emulates the TTY modem-control ioctls
(TIOCMGET/TIOCMSET/...) 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.

This matters for Qualcomm/Sierra SDX55/SDX65 MHI modems (e.g. Sierra
EM9291): they withhold unsolicited AT result codes (URCs such as +CREG,
and OMA-DM/LwM2M session indications) on an AT port until the host
asserts DTR, while still answering solicited commands. mhi_wwan_ctrl
exposes the DUN (AT) ports but had no way to signal DTR, so URCs never
reached userspace -- unlike the TTY-based USB and legacy PCIe drivers for
the same hardware, where the TTY layer raises DTR on open.

Patch 1 adds optional ->tiocmget/->tiocmset ops to wwan_port_ops and
wires them into the AT/QCDM ioctl path (drivers without them keep today's
store-only behaviour). Patch 2 enumerates the IP_CTRL channel -- a
standard Qualcomm MHI control channel, not Sierra-specific -- on the
Sierra em919x/em929x config, which is the device tested. Patch 3
implements ->tiocmset in mhi_wwan_ctrl over IP_CTRL, and mirrors TTY
semantics by raising DTR/RTS on open and dropping on close, so existing
userspace (ModemManager, terminal tools) behaves as it does with the
TTY-based stacks.

The IP_CTRL channel is bound by a second mhi_driver inside mhi_wwan_ctrl
with its own callbacks, leaving the AT/QMI/MBIM data path untouched; the
control device for each MHI controller is tracked per-controller so
multiple modems are supported. The consumer is device-agnostic (it binds
purely by channel name), so any Qualcomm modem whose pci_generic config
enumerates IP_CTRL gets DTR support with no driver change.

Note: the series spans two subsystems -- patch 2 touches drivers/bus/mhi
(MHI BUS), patches 1 and 3 touch drivers/net/wwan and include/linux/wwan.h
(WWAN / netdev). Patch 3 depends on patch 1 at build time; patch 2 is
independent (without it, ->tiocmset simply returns -EIO). Suggest taking
the series through net-next with an Ack from the MHI maintainer on patch 2,
but happy to split if the maintainers prefer.

Tested on a Sierra EM9291 (SDX65) over PCIe/MHI: with the series, opening
/dev/wwan0at0 raises DTR and +CREG / +DMI OMA-DM URCs stream, including a
successful LwM2M server registration; without it, no URCs appear.

Changes in v2 (patch 3 only):
- Use __le32 for mhi_dtr_ctrl_msg fields and cpu_to_le32() on assignment
  (Loic Poulain)
- Use guard(mutex) instead of manual lock/unlock in mhi_wwan_ctrl_send_dtr
  (Loic Poulain)
- Return 0 (not -EIO) when IP_CTRL channel is not enumerated so that
  TIOCMSET does not regress for devices without IP_CTRL (Loic Poulain)

v1: https://lore.kernel.org/netdev/20260804233411.1953445-1-peter.hunt@opengear.com/

Peter Hunt (3):
  net: wwan: core: propagate modem control signals to port drivers
  bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra
    EM919x/EM929x
  net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel

 drivers/bus/mhi/host/pci_generic.c |   2 +
 drivers/net/wwan/mhi_wwan_ctrl.c   | 183 ++++++++++++++++++++++++++++-
 drivers/net/wwan/wwan_core.c       |  12 +-
 include/linux/wwan.h               |   4 +
 4 files changed, 199 insertions(+), 2 deletions(-)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers
  2026-08-06 15:52 [PATCH net-next v2 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
@ 2026-08-06 15:52 ` Peter Hunt
  2026-08-07  7:41   ` Loic Poulain
  2026-08-06 15:52 ` [PATCH net-next v2 2/3] bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra EM919x/EM929x Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Peter Hunt
  2 siblings, 1 reply; 6+ messages in thread
From: Peter Hunt @ 2026-08-06 15:52 UTC (permalink / raw)
  To: mani, loic.poulain, ryazanov.s.a
  Cc: johannes, netdev, mhi, linux-arm-msm, linux-kernel, Peter Hunt

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 optional ->tiocmget/->tiocmset operations to struct wwan_port_ops and
call them from the AT/QCDM ioctl path. ->tiocmset is passed the resolved
TIOCM_* bitmask after BIC/BIS/SET has been applied; ->tiocmget, when
provided, refreshes the cached bits from the device. Drivers that do not
implement them keep the previous store-only behaviour.

Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
 drivers/net/wwan/wwan_core.c | 12 +++++++++++-
 include/linux/wwan.h         |  4 ++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wwan/wwan_core.c b/drivers/net/wwan/wwan_core.c
index ffbcf11e4e68..bf83eb5790fe 100644
--- a/drivers/net/wwan/wwan_core.c
+++ b/drivers/net/wwan/wwan_core.c
@@ -1018,7 +1018,15 @@ static long wwan_port_fops_at_ioctl(struct wwan_port *port, unsigned int cmd,
 #endif
 
 	case TIOCMGET:
-		ret = put_user(port->at_data.mdmbits, (int __user *)arg);
+		if (port->ops->tiocmget) {
+			ret = port->ops->tiocmget(port);
+			if (ret >= 0) {
+				port->at_data.mdmbits = ret;
+				ret = 0;
+			}
+		}
+		if (!ret)
+			ret = put_user(port->at_data.mdmbits, (int __user *)arg);
 		break;
 
 	case TIOCMSET:
@@ -1036,6 +1044,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->ops->tiocmset)
+			ret = port->ops->tiocmset(port, port->at_data.mdmbits);
 		break;
 	}
 
diff --git a/include/linux/wwan.h b/include/linux/wwan.h
index 1e0e2cb53579..bac02521e274 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.
+ * @tiocmget: Optional routine that reads the modem control lines.
+ * @tiocmset: Optional routine that sets the modem control lines.
  *
  * 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,8 @@ 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);
+	int (*tiocmget)(struct wwan_port *port);
+	int (*tiocmset)(struct wwan_port *port, unsigned int mdmbits);
 };
 
 /** struct wwan_port_caps - The WWAN port capbilities
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH net-next v2 2/3] bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra EM919x/EM929x
  2026-08-06 15:52 [PATCH net-next v2 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
@ 2026-08-06 15:52 ` Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Peter Hunt
  2 siblings, 0 replies; 6+ messages in thread
From: Peter Hunt @ 2026-08-06 15:52 UTC (permalink / raw)
  To: mani, loic.poulain, ryazanov.s.a
  Cc: johannes, netdev, mhi, linux-arm-msm, linux-kernel, Peter Hunt

IP_CTRL is a standard Qualcomm MHI control channel (channels 18/19) that
carries the host serial control lines (DTR/RTS) to the modem; it is not
Sierra-specific. It was not enumerated for any device in pci_generic, so
the host had no way to drive those signals.

Enumerate it in the Sierra em919x channel config (shared by the EM919x
and EM929x) using the control event ring. This is the device the change
was tested against; other Qualcomm modem configs that expose IP_CTRL can
enable it the same way, since the mhi_wwan_ctrl DTR support that consumes
it is device-agnostic and binds purely by channel name.

Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
 drivers/bus/mhi/host/pci_generic.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/bus/mhi/host/pci_generic.c b/drivers/bus/mhi/host/pci_generic.c
index 0d0d9c7ffa4b..ba47dd153837 100644
--- a/drivers/bus/mhi/host/pci_generic.c
+++ b/drivers/bus/mhi/host/pci_generic.c
@@ -728,6 +728,8 @@ static const struct mhi_channel_config mhi_sierra_em919x_channels[] = {
 	MHI_CHANNEL_CONFIG_DL(13, "MBIM", 128, 0),
 	MHI_CHANNEL_CONFIG_UL(14, "QMI", 32, 0),
 	MHI_CHANNEL_CONFIG_DL(15, "QMI", 32, 0),
+	MHI_CHANNEL_CONFIG_UL(18, "IP_CTRL", 32, 0),
+	MHI_CHANNEL_CONFIG_DL(19, "IP_CTRL", 32, 0),
 	MHI_CHANNEL_CONFIG_UL(32, "DUN", 32, 0),
 	MHI_CHANNEL_CONFIG_DL(33, "DUN", 32, 0),
 	MHI_CHANNEL_CONFIG_HW_UL(100, "IP_HW0", 512, 1),
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
  2026-08-06 15:52 [PATCH net-next v2 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
  2026-08-06 15:52 ` [PATCH net-next v2 2/3] bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra EM919x/EM929x Peter Hunt
@ 2026-08-06 15:52 ` Peter Hunt
  2026-08-07  7:38   ` Loic Poulain
  2 siblings, 1 reply; 6+ messages in thread
From: Peter Hunt @ 2026-08-06 15:52 UTC (permalink / raw)
  To: mani, loic.poulain, ryazanov.s.a
  Cc: johannes, netdev, mhi, linux-arm-msm, linux-kernel, Peter Hunt

Qualcomm/Sierra SDX55/SDX65 modems (e.g. Sierra EM9291) withhold
unsolicited AT result codes (URCs such as +CREG, and the +DMI OMA-DM/LwM2M
session indications) on an AT port until the host asserts DTR.
mhi_wwan_ctrl exposed the AT (DUN) ports but had no way to signal DTR, so
URCs never reached userspace.

Carry the host serial-control lines to the modem over the dedicated
IP_CTRL MHI channel, which this module now also binds. IP_CTRL uses a
separate mhi_driver with its own callbacks so the AT/QMI/MBIM data path is
untouched; the control-channel device for each MHI controller is tracked
in a small registry so an AT port drives the IP_CTRL channel of its own
modem (multiple modems are supported).

Mirror TTY semantics: an AT port raises DTR/RTS on open and drops them on
close, and the new wwan ->tiocmset op lets userspace change them via
TIOCMSET. This matches the TTY-based USB and legacy PCIe drivers for the
same modems, where DTR is asserted on open. Received device->host serial
state is not needed and is ignored; ->tiocmget is left unimplemented so
the wwan core reports the cached bits.

Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
v2: Use __le32 for mhi_dtr_ctrl_msg fields and cpu_to_le32() on
    assignment. Use guard(mutex) in mhi_wwan_ctrl_send_dtr. Return 0
    when IP_CTRL is not enumerated so TIOCMSET does not regress for
    devices without IP_CTRL.

 drivers/net/wwan/mhi_wwan_ctrl.c | 183 ++++++++++++++++++++++++++++++-
 1 file changed, 182 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
index a31d8540fbb8..95f4f0e6e4aa 100644
--- a/drivers/net/wwan/mhi_wwan_ctrl.c
+++ b/drivers/net/wwan/mhi_wwan_ctrl.c
@@ -1,8 +1,12 @@
 // SPDX-License-Identifier: GPL-2.0-only
 /* Copyright (c) 2021, Linaro Ltd <loic.poulain@linaro.org> */
 #include <linux/kernel.h>
+#include <linux/list.h>
 #include <linux/mhi.h>
 #include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/termios.h>
 #include <linux/wwan.h>
 
 /* MHI wwan flags */
@@ -14,6 +18,30 @@ enum mhi_wwan_flags {
 
 #define MHI_WWAN_MAX_MTU	0x8000
 
+/* IP_CTRL channel message that sets the modem's DTR/RTS control lines */
+struct mhi_dtr_ctrl_msg {
+	__le32 preamble;
+	__le32 msg_id;
+	__le32 dest_id;
+	__le32 size;
+	__le32 msg;
+} __packed;
+
+#define MHI_DTR_CTRL_MAGIC	0x4C525443	/* 'CTRL' */
+#define MHI_DTR_MSG_DTR		BIT(0)
+#define MHI_DTR_MSG_RTS		BIT(1)
+#define MHI_DTR_HOST_STATE	0x10
+
+/* Per-controller IP_CTRL channel, used to signal DTR/RTS to that modem */
+struct mhi_wwan_dtr {
+	struct mhi_controller *cntrl;
+	struct mhi_device *mhi_dev;
+	struct list_head node;
+};
+
+static LIST_HEAD(mhi_wwan_dtr_list);
+static DEFINE_MUTEX(mhi_wwan_dtr_lock);
+
 struct mhi_wwan_dev {
 	/* Lower level is a mhi dev, upper level is a wwan port */
 	struct mhi_device *mhi_dev;
@@ -23,6 +51,9 @@ struct mhi_wwan_dev {
 	unsigned long flags;
 	size_t mtu;
 
+	/* AT (DUN) ports raise DTR/RTS on open to receive unsolicited output */
+	bool is_at_port;
+
 	/* Protect against concurrent TX and TX-completion (bh) */
 	spinlock_t tx_lock;
 
@@ -103,6 +134,55 @@ static void mhi_wwan_ctrl_refill_work(struct work_struct *work)
 	}
 }
 
+/* Signal the modem's DTR/RTS lines over its own controller's IP_CTRL channel */
+static int mhi_wwan_ctrl_send_dtr(struct mhi_wwan_dev *mhiwwan, unsigned int mdmbits)
+{
+	struct mhi_controller *cntrl = mhiwwan->mhi_dev->mhi_cntrl;
+	struct mhi_device *ctrl_dev = NULL;
+	struct mhi_dtr_ctrl_msg *dtr_msg;
+	struct mhi_wwan_dtr *dtr;
+	u32 msg = 0;
+	int ret;
+
+	guard(mutex)(&mhi_wwan_dtr_lock);
+
+	list_for_each_entry(dtr, &mhi_wwan_dtr_list, node) {
+		if (dtr->cntrl == cntrl) {
+			ctrl_dev = dtr->mhi_dev;
+			break;
+		}
+	}
+	if (!ctrl_dev)
+		return 0; /* IP_CTRL not enumerated; best-effort, not an error */
+
+	dtr_msg = kzalloc_obj(*dtr_msg);
+	if (!dtr_msg)
+		return -ENOMEM;
+
+	if (mdmbits & TIOCM_DTR)
+		msg |= MHI_DTR_MSG_DTR;
+	if (mdmbits & TIOCM_RTS)
+		msg |= MHI_DTR_MSG_RTS;
+
+	dtr_msg->preamble = cpu_to_le32(MHI_DTR_CTRL_MAGIC);
+	dtr_msg->msg_id = cpu_to_le32(MHI_DTR_HOST_STATE);
+	dtr_msg->dest_id = cpu_to_le32(mhiwwan->mhi_dev->ul_chan_id);
+	dtr_msg->size = cpu_to_le32(sizeof(__le32));
+	dtr_msg->msg = cpu_to_le32(msg);
+
+	ret = mhi_queue_buf(ctrl_dev, DMA_TO_DEVICE, dtr_msg, sizeof(*dtr_msg),
+			    MHI_EOT);
+	if (ret)
+		kfree(dtr_msg);
+
+	return ret;
+}
+
+static int mhi_wwan_ctrl_tiocmset(struct wwan_port *port, unsigned int mdmbits)
+{
+	return mhi_wwan_ctrl_send_dtr(wwan_port_get_drvdata(port), mdmbits);
+}
+
 static int mhi_wwan_ctrl_start(struct wwan_port *port)
 {
 	struct mhi_wwan_dev *mhiwwan = wwan_port_get_drvdata(port);
@@ -122,6 +202,10 @@ static int mhi_wwan_ctrl_start(struct wwan_port *port)
 		mhi_wwan_ctrl_refill_work(&mhiwwan->rx_refill);
 	}
 
+	/* Raise DTR/RTS on open so the modem forwards unsolicited output */
+	if (mhiwwan->is_at_port)
+		mhi_wwan_ctrl_send_dtr(mhiwwan, TIOCM_DTR | TIOCM_RTS);
+
 	return 0;
 }
 
@@ -135,6 +219,10 @@ static void mhi_wwan_ctrl_stop(struct wwan_port *port)
 
 	cancel_work_sync(&mhiwwan->rx_refill);
 
+	/* Drop DTR/RTS on close */
+	if (mhiwwan->is_at_port)
+		mhi_wwan_ctrl_send_dtr(mhiwwan, 0);
+
 	mhi_unprepare_from_transfer(mhiwwan->mhi_dev);
 }
 
@@ -163,6 +251,7 @@ static const struct wwan_port_ops wwan_pops = {
 	.start = mhi_wwan_ctrl_start,
 	.stop = mhi_wwan_ctrl_stop,
 	.tx = mhi_wwan_ctrl_tx,
+	.tiocmset = mhi_wwan_ctrl_tiocmset,
 };
 
 static void mhi_ul_xfer_cb(struct mhi_device *mhi_dev,
@@ -223,6 +312,7 @@ static int mhi_wwan_ctrl_probe(struct mhi_device *mhi_dev,
 
 	mhiwwan->mhi_dev = mhi_dev;
 	mhiwwan->mtu = MHI_WWAN_MAX_MTU;
+	mhiwwan->is_at_port = (id->driver_data == WWAN_PORT_AT);
 	INIT_WORK(&mhiwwan->rx_refill, mhi_wwan_ctrl_refill_work);
 	spin_lock_init(&mhiwwan->tx_lock);
 	spin_lock_init(&mhiwwan->rx_lock);
@@ -255,6 +345,59 @@ static void mhi_wwan_ctrl_remove(struct mhi_device *mhi_dev)
 	kfree(mhiwwan);
 }
 
+/* IP_CTRL channel driver, bound separately so the data-port path is untouched */
+static void mhi_wwan_dtr_ul_xfer_cb(struct mhi_device *mhi_dev,
+				    struct mhi_result *mhi_result)
+{
+	/* MHI core has done with the buffer, release it */
+	kfree(mhi_result->buf_addr);
+}
+
+static void mhi_wwan_dtr_dl_xfer_cb(struct mhi_device *mhi_dev,
+				    struct mhi_result *mhi_result)
+{
+	/* Modem serial state is not needed, drop it */
+}
+
+static int mhi_wwan_dtr_probe(struct mhi_device *mhi_dev,
+			      const struct mhi_device_id *id)
+{
+	struct mhi_wwan_dtr *dtr;
+	int ret;
+
+	dtr = kzalloc_obj(*dtr);
+	if (!dtr)
+		return -ENOMEM;
+
+	ret = mhi_prepare_for_transfer(mhi_dev);
+	if (ret) {
+		kfree(dtr);
+		return ret;
+	}
+
+	dtr->cntrl = mhi_dev->mhi_cntrl;
+	dtr->mhi_dev = mhi_dev;
+	dev_set_drvdata(&mhi_dev->dev, dtr);
+
+	mutex_lock(&mhi_wwan_dtr_lock);
+	list_add(&dtr->node, &mhi_wwan_dtr_list);
+	mutex_unlock(&mhi_wwan_dtr_lock);
+
+	return 0;
+}
+
+static void mhi_wwan_dtr_remove(struct mhi_device *mhi_dev)
+{
+	struct mhi_wwan_dtr *dtr = dev_get_drvdata(&mhi_dev->dev);
+
+	mutex_lock(&mhi_wwan_dtr_lock);
+	list_del(&dtr->node);
+	mutex_unlock(&mhi_wwan_dtr_lock);
+
+	mhi_unprepare_from_transfer(mhi_dev);
+	kfree(dtr);
+}
+
 static const struct mhi_device_id mhi_wwan_ctrl_match_table[] = {
 	{ .chan = "DUN", .driver_data = WWAN_PORT_AT },
 	{ .chan = "DUN2", .driver_data = WWAN_PORT_AT },
@@ -278,7 +421,45 @@ static struct mhi_driver mhi_wwan_ctrl_driver = {
 	},
 };
 
-module_mhi_driver(mhi_wwan_ctrl_driver);
+static const struct mhi_device_id mhi_wwan_dtr_match_table[] = {
+	{ .chan = "IP_CTRL" },
+	{},
+};
+MODULE_DEVICE_TABLE(mhi, mhi_wwan_dtr_match_table);
+
+static struct mhi_driver mhi_wwan_dtr_driver = {
+	.id_table = mhi_wwan_dtr_match_table,
+	.remove = mhi_wwan_dtr_remove,
+	.probe = mhi_wwan_dtr_probe,
+	.ul_xfer_cb = mhi_wwan_dtr_ul_xfer_cb,
+	.dl_xfer_cb = mhi_wwan_dtr_dl_xfer_cb,
+	.driver = {
+		.name = "mhi_wwan_dtr",
+	},
+};
+
+static int __init mhi_wwan_ctrl_init(void)
+{
+	int ret;
+
+	ret = mhi_driver_register(&mhi_wwan_dtr_driver);
+	if (ret)
+		return ret;
+
+	ret = mhi_driver_register(&mhi_wwan_ctrl_driver);
+	if (ret)
+		mhi_driver_unregister(&mhi_wwan_dtr_driver);
+
+	return ret;
+}
+module_init(mhi_wwan_ctrl_init);
+
+static void __exit mhi_wwan_ctrl_exit(void)
+{
+	mhi_driver_unregister(&mhi_wwan_ctrl_driver);
+	mhi_driver_unregister(&mhi_wwan_dtr_driver);
+}
+module_exit(mhi_wwan_ctrl_exit);
 
 MODULE_LICENSE("GPL v2");
 MODULE_DESCRIPTION("MHI WWAN CTRL Driver");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
  2026-08-06 15:52 ` [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Peter Hunt
@ 2026-08-07  7:38   ` Loic Poulain
  0 siblings, 0 replies; 6+ messages in thread
From: Loic Poulain @ 2026-08-07  7:38 UTC (permalink / raw)
  To: Peter Hunt
  Cc: mani, ryazanov.s.a, johannes, netdev, mhi, linux-arm-msm,
	linux-kernel

On Thu, Aug 6, 2026 at 5:54 PM Peter Hunt <peter.hunt@opengear.com> wrote:
>
> Qualcomm/Sierra SDX55/SDX65 modems (e.g. Sierra EM9291) withhold
> unsolicited AT result codes (URCs such as +CREG, and the +DMI OMA-DM/LwM2M
> session indications) on an AT port until the host asserts DTR.
> mhi_wwan_ctrl exposed the AT (DUN) ports but had no way to signal DTR, so
> URCs never reached userspace.
>
> Carry the host serial-control lines to the modem over the dedicated
> IP_CTRL MHI channel, which this module now also binds. IP_CTRL uses a
> separate mhi_driver with its own callbacks so the AT/QMI/MBIM data path is
> untouched; the control-channel device for each MHI controller is tracked
> in a small registry so an AT port drives the IP_CTRL channel of its own
> modem (multiple modems are supported).
>
> Mirror TTY semantics: an AT port raises DTR/RTS on open and drops them on
> close, and the new wwan ->tiocmset op lets userspace change them via
> TIOCMSET. This matches the TTY-based USB and legacy PCIe drivers for the
> same modems, where DTR is asserted on open. Received device->host serial
> state is not needed and is ignored; ->tiocmget is left unimplemented so
> the wwan core reports the cached bits.
>
> Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
> ---
> v2: Use __le32 for mhi_dtr_ctrl_msg fields and cpu_to_le32() on
>     assignment. Use guard(mutex) in mhi_wwan_ctrl_send_dtr. Return 0
>     when IP_CTRL is not enumerated so TIOCMSET does not regress for
>     devices without IP_CTRL.
>
>  drivers/net/wwan/mhi_wwan_ctrl.c | 183 ++++++++++++++++++++++++++++++-
>  1 file changed, 182 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
> index a31d8540fbb8..95f4f0e6e4aa 100644
> --- a/drivers/net/wwan/mhi_wwan_ctrl.c
> +++ b/drivers/net/wwan/mhi_wwan_ctrl.c
> @@ -1,8 +1,12 @@
>  // SPDX-License-Identifier: GPL-2.0-only
>  /* Copyright (c) 2021, Linaro Ltd <loic.poulain@linaro.org> */
>  #include <linux/kernel.h>
> +#include <linux/list.h>
>  #include <linux/mhi.h>
>  #include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/slab.h>
> +#include <linux/termios.h>
>  #include <linux/wwan.h>
>
>  /* MHI wwan flags */
> @@ -14,6 +18,30 @@ enum mhi_wwan_flags {
>
>  #define MHI_WWAN_MAX_MTU       0x8000
>
> +/* IP_CTRL channel message that sets the modem's DTR/RTS control lines */
> +struct mhi_dtr_ctrl_msg {
> +       __le32 preamble;
> +       __le32 msg_id;
> +       __le32 dest_id;
> +       __le32 size;
> +       __le32 msg;
> +} __packed;
> +
> +#define MHI_DTR_CTRL_MAGIC     0x4C525443      /* 'CTRL' */
> +#define MHI_DTR_MSG_DTR                BIT(0)
> +#define MHI_DTR_MSG_RTS                BIT(1)
> +#define MHI_DTR_HOST_STATE     0x10
> +
> +/* Per-controller IP_CTRL channel, used to signal DTR/RTS to that modem */
> +struct mhi_wwan_dtr {
> +       struct mhi_controller *cntrl;
> +       struct mhi_device *mhi_dev;
> +       struct list_head node;
> +};
> +
> +static LIST_HEAD(mhi_wwan_dtr_list);
> +static DEFINE_MUTEX(mhi_wwan_dtr_lock);
> +
>  struct mhi_wwan_dev {
>         /* Lower level is a mhi dev, upper level is a wwan port */
>         struct mhi_device *mhi_dev;
> @@ -23,6 +51,9 @@ struct mhi_wwan_dev {
>         unsigned long flags;
>         size_t mtu;
>
> +       /* AT (DUN) ports raise DTR/RTS on open to receive unsolicited output */
> +       bool is_at_port;

Do we really need that? the wwan core already has this knowledge and
so will only forward/support tiocm ops to AT ports. Also this info can
be retrieved from the wwan_port type.

> +
>         /* Protect against concurrent TX and TX-completion (bh) */
>         spinlock_t tx_lock;
>
> @@ -103,6 +134,55 @@ static void mhi_wwan_ctrl_refill_work(struct work_struct *work)
>         }
>  }
>
> +/* Signal the modem's DTR/RTS lines over its own controller's IP_CTRL channel */
> +static int mhi_wwan_ctrl_send_dtr(struct mhi_wwan_dev *mhiwwan, unsigned int mdmbits)
> +{
> +       struct mhi_controller *cntrl = mhiwwan->mhi_dev->mhi_cntrl;
> +       struct mhi_device *ctrl_dev = NULL;
> +       struct mhi_dtr_ctrl_msg *dtr_msg;
> +       struct mhi_wwan_dtr *dtr;
> +       u32 msg = 0;
> +       int ret;
> +
> +       guard(mutex)(&mhi_wwan_dtr_lock);
> +
> +       list_for_each_entry(dtr, &mhi_wwan_dtr_list, node) {
> +               if (dtr->cntrl == cntrl) {
> +                       ctrl_dev = dtr->mhi_dev;
> +                       break;
> +               }
> +       }
> +       if (!ctrl_dev)
> +               return 0; /* IP_CTRL not enumerated; best-effort, not an error */
> +
> +       dtr_msg = kzalloc_obj(*dtr_msg);
> +       if (!dtr_msg)
> +               return -ENOMEM;
> +
> +       if (mdmbits & TIOCM_DTR)
> +               msg |= MHI_DTR_MSG_DTR;
> +       if (mdmbits & TIOCM_RTS)
> +               msg |= MHI_DTR_MSG_RTS;
> +
> +       dtr_msg->preamble = cpu_to_le32(MHI_DTR_CTRL_MAGIC);
> +       dtr_msg->msg_id = cpu_to_le32(MHI_DTR_HOST_STATE);
> +       dtr_msg->dest_id = cpu_to_le32(mhiwwan->mhi_dev->ul_chan_id);
> +       dtr_msg->size = cpu_to_le32(sizeof(__le32));
> +       dtr_msg->msg = cpu_to_le32(msg);
> +
> +       ret = mhi_queue_buf(ctrl_dev, DMA_TO_DEVICE, dtr_msg, sizeof(*dtr_msg),
> +                           MHI_EOT);
> +       if (ret)
> +               kfree(dtr_msg);
> +
> +       return ret;
> +}
> +
> +static int mhi_wwan_ctrl_tiocmset(struct wwan_port *port, unsigned int mdmbits)
> +{
> +       return mhi_wwan_ctrl_send_dtr(wwan_port_get_drvdata(port), mdmbits);
> +}
> +
>  static int mhi_wwan_ctrl_start(struct wwan_port *port)
>  {
>         struct mhi_wwan_dev *mhiwwan = wwan_port_get_drvdata(port);
> @@ -122,6 +202,10 @@ static int mhi_wwan_ctrl_start(struct wwan_port *port)
>                 mhi_wwan_ctrl_refill_work(&mhiwwan->rx_refill);
>         }
>
> +       /* Raise DTR/RTS on open so the modem forwards unsolicited output */
> +       if (mhiwwan->is_at_port)
> +               mhi_wwan_ctrl_send_dtr(mhiwwan, TIOCM_DTR | TIOCM_RTS);

Why not handling this enablement directly in the wwan core?

> +
>         return 0;
>  }
>
> @@ -135,6 +219,10 @@ static void mhi_wwan_ctrl_stop(struct wwan_port *port)
>
>         cancel_work_sync(&mhiwwan->rx_refill);
>
> +       /* Drop DTR/RTS on close */
> +       if (mhiwwan->is_at_port)
> +               mhi_wwan_ctrl_send_dtr(mhiwwan, 0);
> +
>         mhi_unprepare_from_transfer(mhiwwan->mhi_dev);
>  }
>
> @@ -163,6 +251,7 @@ static const struct wwan_port_ops wwan_pops = {
>         .start = mhi_wwan_ctrl_start,
>         .stop = mhi_wwan_ctrl_stop,
>         .tx = mhi_wwan_ctrl_tx,
> +       .tiocmset = mhi_wwan_ctrl_tiocmset,
>  };
>
>  static void mhi_ul_xfer_cb(struct mhi_device *mhi_dev,
> @@ -223,6 +312,7 @@ static int mhi_wwan_ctrl_probe(struct mhi_device *mhi_dev,
>
>         mhiwwan->mhi_dev = mhi_dev;
>         mhiwwan->mtu = MHI_WWAN_MAX_MTU;
> +       mhiwwan->is_at_port = (id->driver_data == WWAN_PORT_AT);
>         INIT_WORK(&mhiwwan->rx_refill, mhi_wwan_ctrl_refill_work);
>         spin_lock_init(&mhiwwan->tx_lock);
>         spin_lock_init(&mhiwwan->rx_lock);
> @@ -255,6 +345,59 @@ static void mhi_wwan_ctrl_remove(struct mhi_device *mhi_dev)
>         kfree(mhiwwan);
>  }
>
> +/* IP_CTRL channel driver, bound separately so the data-port path is untouched */
> +static void mhi_wwan_dtr_ul_xfer_cb(struct mhi_device *mhi_dev,
> +                                   struct mhi_result *mhi_result)
> +{
> +       /* MHI core has done with the buffer, release it */
> +       kfree(mhi_result->buf_addr);
> +}
> +
> +static void mhi_wwan_dtr_dl_xfer_cb(struct mhi_device *mhi_dev,
> +                                   struct mhi_result *mhi_result)
> +{
> +       /* Modem serial state is not needed, drop it */
> +}
> +
> +static int mhi_wwan_dtr_probe(struct mhi_device *mhi_dev,
> +                             const struct mhi_device_id *id)
> +{
> +       struct mhi_wwan_dtr *dtr;
> +       int ret;
> +
> +       dtr = kzalloc_obj(*dtr);
> +       if (!dtr)
> +               return -ENOMEM;
> +
> +       ret = mhi_prepare_for_transfer(mhi_dev);
> +       if (ret) {
> +               kfree(dtr);
> +               return ret;
> +       }
> +
> +       dtr->cntrl = mhi_dev->mhi_cntrl;
> +       dtr->mhi_dev = mhi_dev;
> +       dev_set_drvdata(&mhi_dev->dev, dtr);
> +
> +       mutex_lock(&mhi_wwan_dtr_lock);
> +       list_add(&dtr->node, &mhi_wwan_dtr_list);
> +       mutex_unlock(&mhi_wwan_dtr_lock);
> +
> +       return 0;
> +}
> +
> +static void mhi_wwan_dtr_remove(struct mhi_device *mhi_dev)
> +{
> +       struct mhi_wwan_dtr *dtr = dev_get_drvdata(&mhi_dev->dev);
> +
> +       mutex_lock(&mhi_wwan_dtr_lock);
> +       list_del(&dtr->node);
> +       mutex_unlock(&mhi_wwan_dtr_lock);
> +
> +       mhi_unprepare_from_transfer(mhi_dev);
> +       kfree(dtr);
> +}
> +
>  static const struct mhi_device_id mhi_wwan_ctrl_match_table[] = {
>         { .chan = "DUN", .driver_data = WWAN_PORT_AT },
>         { .chan = "DUN2", .driver_data = WWAN_PORT_AT },
> @@ -278,7 +421,45 @@ static struct mhi_driver mhi_wwan_ctrl_driver = {
>         },
>  };
>
> -module_mhi_driver(mhi_wwan_ctrl_driver);
> +static const struct mhi_device_id mhi_wwan_dtr_match_table[] = {
> +       { .chan = "IP_CTRL" },
> +       {},
> +};
> +MODULE_DEVICE_TABLE(mhi, mhi_wwan_dtr_match_table);
> +
> +static struct mhi_driver mhi_wwan_dtr_driver = {
> +       .id_table = mhi_wwan_dtr_match_table,
> +       .remove = mhi_wwan_dtr_remove,
> +       .probe = mhi_wwan_dtr_probe,
> +       .ul_xfer_cb = mhi_wwan_dtr_ul_xfer_cb,
> +       .dl_xfer_cb = mhi_wwan_dtr_dl_xfer_cb,
> +       .driver = {
> +               .name = "mhi_wwan_dtr",
> +       },
> +};
> +
> +static int __init mhi_wwan_ctrl_init(void)
> +{
> +       int ret;
> +
> +       ret = mhi_driver_register(&mhi_wwan_dtr_driver);
> +       if (ret)
> +               return ret;
> +
> +       ret = mhi_driver_register(&mhi_wwan_ctrl_driver);
> +       if (ret)
> +               mhi_driver_unregister(&mhi_wwan_dtr_driver);
> +
> +       return ret;
> +}
> +module_init(mhi_wwan_ctrl_init);
> +
> +static void __exit mhi_wwan_ctrl_exit(void)
> +{
> +       mhi_driver_unregister(&mhi_wwan_ctrl_driver);
> +       mhi_driver_unregister(&mhi_wwan_dtr_driver);
> +}
> +module_exit(mhi_wwan_ctrl_exit);
>
>  MODULE_LICENSE("GPL v2");
>  MODULE_DESCRIPTION("MHI WWAN CTRL Driver");
> --
> 2.43.0
>

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers
  2026-08-06 15:52 ` [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
@ 2026-08-07  7:41   ` Loic Poulain
  0 siblings, 0 replies; 6+ messages in thread
From: Loic Poulain @ 2026-08-07  7:41 UTC (permalink / raw)
  To: Peter Hunt
  Cc: mani, ryazanov.s.a, johannes, netdev, mhi, linux-arm-msm,
	linux-kernel

On Thu, Aug 6, 2026 at 5:54 PM Peter Hunt <peter.hunt@opengear.com> 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 optional ->tiocmget/->tiocmset operations to struct wwan_port_ops and
> call them from the AT/QCDM ioctl path. ->tiocmset is passed the resolved
> TIOCM_* bitmask after BIC/BIS/SET has been applied; ->tiocmget, when
> provided, refreshes the cached bits from the device. Drivers that do not
> implement them keep the previous store-only behaviour.
>
> Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
> ---
>  drivers/net/wwan/wwan_core.c | 12 +++++++++++-
>  include/linux/wwan.h         |  4 ++++
>  2 files changed, 15 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/net/wwan/wwan_core.c b/drivers/net/wwan/wwan_core.c
> index ffbcf11e4e68..bf83eb5790fe 100644
> --- a/drivers/net/wwan/wwan_core.c
> +++ b/drivers/net/wwan/wwan_core.c
> @@ -1018,7 +1018,15 @@ static long wwan_port_fops_at_ioctl(struct wwan_port *port, unsigned int cmd,
>  #endif
>
>         case TIOCMGET:
> -               ret = put_user(port->at_data.mdmbits, (int __user *)arg);
> +               if (port->ops->tiocmget) {
> +                       ret = port->ops->tiocmget(port);
> +                       if (ret >= 0) {
> +                               port->at_data.mdmbits = ret;
> +                               ret = 0;
> +                       }
> +               }
> +               if (!ret)
> +                       ret = put_user(port->at_data.mdmbits, (int __user *)arg);
>                 break;
>
>         case TIOCMSET:
> @@ -1036,6 +1044,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->ops->tiocmset)
> +                       ret = port->ops->tiocmset(port, port->at_data.mdmbits);
>                 break;
>         }
>
> diff --git a/include/linux/wwan.h b/include/linux/wwan.h
> index 1e0e2cb53579..bac02521e274 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.
> + * @tiocmget: Optional routine that reads the modem control lines.
> + * @tiocmset: Optional routine that sets the modem control lines.
>   *
>   * 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,8 @@ 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);
> +       int (*tiocmget)(struct wwan_port *port);

You don't use it in this series, so please drop.

> +       int (*tiocmset)(struct wwan_port *port, unsigned int mdmbits);

TBH, I would prefer a dtr_rts(port, on/off) callback like
tty_port_operations. the tiocmset/get generic handling can stay in the
WWAN core.

>  };
>
>  /** struct wwan_port_caps - The WWAN port capbilities
> --
> 2.43.0
>

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-07  7:41 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-06 15:52 [PATCH net-next v2 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
2026-08-06 15:52 ` [PATCH net-next v2 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
2026-08-07  7:41   ` Loic Poulain
2026-08-06 15:52 ` [PATCH net-next v2 2/3] bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra EM919x/EM929x Peter Hunt
2026-08-06 15:52 ` [PATCH net-next v2 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Peter Hunt
2026-08-07  7:38   ` Loic Poulain

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox