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 v3 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
Date: Fri, 7 Aug 2026 15:50:41 -0600 [thread overview]
Message-ID: <20260807215042.2714442-4-peter.hunt@opengear.com> (raw)
In-Reply-To: <20260807215042.2714442-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>
---
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 | 172 ++++++++++++++++++++++++++++++-
1 file changed, 171 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
index a31d8540fbb8..3aef90ec2176 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,56 @@ 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 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 +241,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 +334,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 +410,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
prev parent reply other threads:[~2026-08-07 21:55 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-07 21:50 [PATCH net-next v3 0/3] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
2026-08-07 21:50 ` [PATCH net-next v3 1/3] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
2026-08-07 21:50 ` Peter Hunt [this message]
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=20260807215042.2714442-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox