[PATCH net-next 3/3] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
Peter Hunt <[email protected]> Tue, 4 Aug 2026 17:34:11 -0600
| Newsgroups | org.kernel.vger.linux-arm-msm,dev.linux.lists.mhi,org.kernel.vger.linux-kernel,org.kernel.vger.netdev |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- drivers/net/wwan/mhi_wwan_ctrl.c | 186 ++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c index a31d8540fbb8..00725904333a 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 <[email protected]> */ #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 { + u32 preamble; + u32 msg_id; + u32 dest_id; + u32 size; + u32 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,58 @@ 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; + int ret; + + mutex_lock(&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) { + mutex_unlock(&mhi_wwan_dtr_lock); + return -EIO; + } + + dtr_msg = kzalloc_obj(*dtr_msg); + if (!dtr_msg) { + mutex_unlock(&mhi_wwan_dtr_lock); + return -ENOMEM; + } + + dtr_msg->preamble = MHI_DTR_CTRL_MAGIC; + dtr_msg->msg_id = MHI_DTR_HOST_STATE; + dtr_msg->dest_id = mhiwwan->mhi_dev->ul_chan_id; + dtr_msg->size = sizeof(u32); + if (mdmbits & TIOCM_DTR) + dtr_msg->msg |= MHI_DTR_MSG_DTR; + if (mdmbits & TIOCM_RTS) + dtr_msg->msg |= MHI_DTR_MSG_RTS; + + ret = mhi_queue_buf(ctrl_dev, DMA_TO_DEVICE, dtr_msg, sizeof(*dtr_msg), + MHI_EOT); + if (ret) + kfree(dtr_msg); + + mutex_unlock(&mhi_wwan_dtr_lock); + + 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 +205,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 +222,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 +254,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 +315,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 +348,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 +424,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