From: Peter Hunt <peter.hunt@opengear.com>
To: loic.poulain@oss.qualcomm.com, ryazanov.s.a@gmail.com, mani@kernel.org
Cc: johannes@sipsolutions.net, netdev@vger.kernel.org,
mhi@lists.linux.dev, linux-arm-msm@vger.kernel.org,
linux-kernel@vger.kernel.org,
Peter Hunt <peter.hunt@opengear.com>
Subject: [PATCH net-next v4 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
Date: Sun, 16 Aug 2026 06:17:05 -0600 [thread overview]
Message-ID: <20260816121705.858013-4-peter.hunt@opengear.com> (raw)
In-Reply-To: <20260816121705.858013-1-peter.hunt@opengear.com>
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).
The wwan core (patch 1) raises DTR/RTS on first open and drops them on
last close for any AT port whose driver implements ->dtr_rts, so no
open/close handling is needed here. The new ->dtr_rts op lets userspace
assert or de-assert them via TIOCMSET/TIOCMBIC/TIOCMBIS. Received
device->host serial state is not needed and is ignored.
Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
v4: Add dev_dbg when IP_CTRL channel is not enumerated for this controller
and when mhi_queue_buf fails, to aid diagnosis of the "URCs missing"
case on controllers that do not declare IP_CTRL
v3: Remove is_at_port; implement ->dtr_rts instead of ->tiocmset;
open/close DTR raise/drop handled by wwan core (Loic Poulain)
---
drivers/net/wwan/mhi_wwan_ctrl.c | 178 ++++++++++++++++++++++++++++++-
1 file changed, 177 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
index a31d8540fbb8..a917df9bb17b 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;
@@ -103,6 +131,62 @@ 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) {
+ dev_dbg(&mhiwwan->mhi_dev->dev,
+ "IP_CTRL not enumerated; DTR/RTS not signalled to modem\n");
+ return 0;
+ }
+
+ 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) {
+ dev_dbg(&mhiwwan->mhi_dev->dev,
+ "failed to queue DTR/RTS signal: %d\n", ret);
+ kfree(dtr_msg);
+ }
+
+ return ret;
+}
+
+static void mhi_wwan_ctrl_dtr_rts(struct wwan_port *port, bool on)
+{
+ mhi_wwan_ctrl_send_dtr(wwan_port_get_drvdata(port),
+ on ? TIOCM_DTR | TIOCM_RTS : 0);
+}
+
static int mhi_wwan_ctrl_start(struct wwan_port *port)
{
struct mhi_wwan_dev *mhiwwan = wwan_port_get_drvdata(port);
@@ -163,6 +247,7 @@ static const struct wwan_port_ops wwan_pops = {
.start = mhi_wwan_ctrl_start,
.stop = mhi_wwan_ctrl_stop,
.tx = mhi_wwan_ctrl_tx,
+ .dtr_rts = mhi_wwan_ctrl_dtr_rts,
};
static void mhi_ul_xfer_cb(struct mhi_device *mhi_dev,
@@ -255,6 +340,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 +416,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
next prev parent reply other threads:[~2026-08-16 12:18 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-16 12:17 [PATCH net-next v4 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
2026-08-16 12:17 ` [PATCH net-next v4 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
2026-08-18 17:09 ` Jakub Kicinski
2026-08-16 12:17 ` [PATCH net-next v4 2/3] bus: mhi: host: pci_generic: enumerate IP_CTRL channel for Sierra EM919x/EM929x Peter Hunt
2026-08-16 12:17 ` Peter Hunt [this message]
2026-08-18 17:09 ` [PATCH net-next v4 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Jakub Kicinski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260816121705.858013-4-peter.hunt@opengear.com \
--to=peter.hunt@opengear.com \
--cc=johannes@sipsolutions.net \
--cc=linux-arm-msm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=loic.poulain@oss.qualcomm.com \
--cc=mani@kernel.org \
--cc=mhi@lists.linux.dev \
--cc=netdev@vger.kernel.org \
--cc=ryazanov.s.a@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.