* [PATCH net-next v8 1/2] net: wwan: core: propagate modem control signals to port drivers
2026-10-07 18:22 [PATCH net-next v8 0/2] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
@ 2026-10-07 18:22 ` Peter Hunt
2026-10-07 18:22 ` [PATCH net-next v8 2/2] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel Peter Hunt
1 sibling, 0 replies; 3+ messages in thread
From: Peter Hunt @ 2026-10-07 18:22 UTC (permalink / raw)
To: loic.poulain, ryazanov.s.a
Cc: johannes, mani, andrew+netdev, davem, edumazet, kuba, pabeni,
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 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. The raise and drop paths go through two small helpers, which do
the AT port and ->dtr_rts checks and pass the driver the resulting
bitmask.
Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
v8: Add wwan_port_raise_dtr_rts() as the counterpart of
wwan_port_drop_dtr_rts() and move the open-path handling there, move
the AT port and ->dtr_rts checks into both helpers, use
scoped_guard() for data_lock (Loic Poulain)
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 | 69 ++++++++++++++++++++++++++++++++++--
include/linux/wwan.h | 3 ++
2 files changed, 70 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wwan/wwan_core.c b/drivers/net/wwan/wwan_core.c
index ffbcf11e4e68..4206649d4b45 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,52 @@ struct wwan_port *wwan_create_port(struct device *parent,
}
EXPORT_SYMBOL_GPL(wwan_create_port);
+/* Raise DTR/RTS on first open of an AT port, as a TTY does. Called with
+ * ops_lock held.
+ */
+static void wwan_port_raise_dtr_rts(struct wwan_port *port)
+{
+ unsigned int bits;
+
+ if (port->type != WWAN_PORT_AT || !port->ops->dtr_rts)
+ return;
+
+ scoped_guard(mutex, &port->data_lock) {
+ port->at_data.mdmbits |= TIOCM_DTR | TIOCM_RTS;
+ bits = port->at_data.mdmbits;
+ }
+
+ port->ops->dtr_rts(port, bits);
+}
+
+/* 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;
+
+ if (port->type != WWAN_PORT_AT || !port->ops->dtr_rts)
+ return;
+
+ scoped_guard(mutex, &port->data_lock) {
+ if (hupcl_only && !(port->at_data.termios.c_cflag & HUPCL))
+ return;
+ port->at_data.mdmbits &= ~(TIOCM_DTR | TIOCM_RTS);
+ bits = port->at_data.mdmbits;
+ }
+
+ 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) {
+ wwan_port_drop_dtr_rts(port, false);
port->ops->stop(port);
port->start_count = 0;
}
@@ -759,8 +803,11 @@ 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++;
+ if (port->start_count == 1)
+ wwan_port_raise_dtr_rts(port);
+ }
out_unlock:
mutex_unlock(&port->ops_lock);
@@ -773,8 +820,10 @@ static void wwan_port_op_stop(struct wwan_port *port)
mutex_lock(&port->ops_lock);
port->start_count--;
if (!port->start_count) {
- if (port->ops)
+ if (port->ops) {
+ wwan_port_drop_dtr_rts(port, true);
port->ops->stop(port);
+ }
skb_queue_purge(&port->rxq);
clear_bit(WWAN_PORT_EXCLUSIVE, &port->flags);
}
@@ -980,6 +1029,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 +1086,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 +1113,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
^ permalink raw reply [flat|nested] 3+ messages in thread* [PATCH net-next v8 2/2] net: wwan: mhi_wwan_ctrl: drive DTR/RTS via the IP_CTRL channel
2026-10-07 18:22 [PATCH net-next v8 0/2] net: wwan: support DTR/RTS on AT ports via MHI IP_CTRL Peter Hunt
2026-10-07 18:22 ` [PATCH net-next v8 1/2] net: wwan: core: propagate modem control signals to port drivers Peter Hunt
@ 2026-10-07 18:22 ` Peter Hunt
1 sibling, 0 replies; 3+ messages in thread
From: Peter Hunt @ 2026-10-07 18:22 UTC (permalink / raw)
To: loic.poulain, ryazanov.s.a
Cc: johannes, mani, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev, mhi, linux-arm-msm, linux-kernel, Peter Hunt
Qualcomm/Sierra SDX55/SDX65 modems 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 IP_CTRL channel has to be declared in the controller's channel config
for this driver to bind. For the Sierra EM919x/EM929x this is done by
commit 83c29a55b89e ("bus: mhi: host: pci_generic: Add IP_CTRL channel
for Sierra EM919x/EM929x") in the MHI tree. On controllers that do not
declare IP_CTRL the driver does not bind and ->dtr_rts is a no-op.
The wwan core (patch 1) raises DTR/RTS on first open and drops them on
last close (if HUPCL is set) 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 messages
are silently discarded; a single recycled sink buffer keeps the IP_CTRL
DL ring live so the modem's transmit path does not stall.
->dtr_rts is void, following the tty_port_operations model it is modelled
on. Delivery is best-effort: at_data.mdmbits always reflects the committed
userspace intent for TIOCMGET regardless of whether the IP_CTRL message was
queued, and the next ioctl or open/close cycle will resynchronise the modem
state. If requeueing the DL sink buffer fails with a transient -ENOMEM it
is retried from a work item, so the DL ring does not stay empty.
Signed-off-by: Peter Hunt <peter.hunt@opengear.com>
---
v8: No changes
v7: Retry a failed DL sink buffer requeue from a delayed work item on
-ENOMEM, and log other failures. Note the dependency on the IP_CTRL
channel entry in pci_generic, now in the MHI tree. Both from the
Sashiko review of v6
v6: Rebase onto net-next. Allocate the IP_CTRL DL sink buffer separately
instead of embedding it in struct mhi_wwan_dtr, so DMA cache
maintenance on non-coherent platforms cannot touch neighbouring fields.
Do not requeue the sink buffer from the DL callback when the transfer
completes with an error (e.g. -ENOTCONN during channel teardown)
v5: Implement ->dtr_rts(port, mdmbits) and drive DTR and RTS
independently. Pre-queue an RX sink buffer in probe and requeue it in
the DL callback so the IP_CTRL DL ring does not run empty and stall
the modem's transmit path
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 | 239 ++++++++++++++++++++++++++++++-
1 file changed, 238 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
index a31d8540fbb8..feb9c03b6392 100644
--- a/drivers/net/wwan/mhi_wwan_ctrl.c
+++ b/drivers/net/wwan/mhi_wwan_ctrl.c
@@ -1,8 +1,13 @@
// 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/workqueue.h>
#include <linux/wwan.h>
/* MHI wwan flags */
@@ -14,6 +19,32 @@ 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;
+ struct mhi_dtr_ctrl_msg *rx_buf; /* DL sink, separate allocation for DMA safety */
+ struct delayed_work rx_refill; /* retries a failed sink buffer requeue */
+};
+
+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 +134,61 @@ 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, unsigned int mdmbits)
+{
+ 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);
@@ -163,6 +249,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 +342,118 @@ 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);
+}
+
+/* Requeue the single DL sink buffer so the IP_CTRL DL ring never runs empty.
+ * A transient mapping failure is retried from a work item, other errors mean
+ * the channel is going away and the next probe queues a fresh buffer.
+ */
+static void mhi_wwan_dtr_queue_rx(struct mhi_wwan_dtr *dtr)
+{
+ int ret;
+
+ ret = mhi_queue_buf(dtr->mhi_dev, DMA_FROM_DEVICE, dtr->rx_buf,
+ sizeof(*dtr->rx_buf), MHI_EOT);
+ if (ret == -ENOMEM) {
+ dev_warn_ratelimited(&dtr->mhi_dev->dev,
+ "failed to requeue IP_CTRL RX buffer, retrying\n");
+ schedule_delayed_work(&dtr->rx_refill, msecs_to_jiffies(100));
+ } else if (ret) {
+ dev_dbg(&dtr->mhi_dev->dev,
+ "failed to requeue IP_CTRL RX buffer: %d\n", ret);
+ }
+}
+
+static void mhi_wwan_dtr_refill_work(struct work_struct *work)
+{
+ struct mhi_wwan_dtr *dtr = container_of(to_delayed_work(work),
+ struct mhi_wwan_dtr, rx_refill);
+
+ mhi_wwan_dtr_queue_rx(dtr);
+}
+
+static void mhi_wwan_dtr_dl_xfer_cb(struct mhi_device *mhi_dev,
+ struct mhi_result *mhi_result)
+{
+ struct mhi_wwan_dtr *dtr = dev_get_drvdata(&mhi_dev->dev);
+
+ /* Channel is being torn down (e.g. -ENOTCONN), do not requeue */
+ if (mhi_result->transaction_status &&
+ mhi_result->transaction_status != -EOVERFLOW)
+ return;
+
+ /* Modem serial state not needed, requeue the sink buffer to keep DL ring live */
+ mhi_wwan_dtr_queue_rx(dtr);
+}
+
+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;
+
+ dtr->rx_buf = kmalloc_obj(*dtr->rx_buf);
+ if (!dtr->rx_buf) {
+ ret = -ENOMEM;
+ goto err_free_dtr;
+ }
+ INIT_DELAYED_WORK(&dtr->rx_refill, mhi_wwan_dtr_refill_work);
+
+ ret = mhi_prepare_for_transfer(mhi_dev);
+ if (ret)
+ goto err_free_buf;
+
+ dtr->cntrl = mhi_dev->mhi_cntrl;
+ dtr->mhi_dev = mhi_dev;
+ dev_set_drvdata(&mhi_dev->dev, dtr);
+
+ ret = mhi_queue_buf(mhi_dev, DMA_FROM_DEVICE, dtr->rx_buf,
+ sizeof(*dtr->rx_buf), MHI_EOT);
+ if (ret)
+ goto err_unprepare;
+
+ mutex_lock(&mhi_wwan_dtr_lock);
+ list_add(&dtr->node, &mhi_wwan_dtr_list);
+ mutex_unlock(&mhi_wwan_dtr_lock);
+
+ return 0;
+
+err_unprepare:
+ mhi_unprepare_from_transfer(mhi_dev);
+err_free_buf:
+ kfree(dtr->rx_buf);
+err_free_dtr:
+ kfree(dtr);
+ return ret;
+}
+
+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);
+ /* No DL callbacks after unprepare, and a pending retry now fails on
+ * the disabled channel without rescheduling
+ */
+ cancel_delayed_work_sync(&dtr->rx_refill);
+ kfree(dtr->rx_buf);
+ 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 +477,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] 3+ messages in thread