* [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver
@ 2026-08-11 7:14 Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
` (7 more replies)
0 siblings, 8 replies; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
T9XX is the PCIe host device driver for MediaTek's
t900 modem. The driver uses the WWAN framework
infrastructure to create the following control ports
and network interfaces for data transactions.
* /dev/wwan0at0 - Interface that supports AT commands.
* /dev/wwan0mbim0 - Interface conforming to the MBIM
protocol.
* wwan0-X - Primary network interface for IP traffic.
The main blocks in the T9XX driver are:
* HW layer - Abstracts the hardware bus operations for
the device, and provides generic interfaces for the
transaction layer to get the device's information and
control the device's behavior. It includes:
* PCIe - Implements probe, removal and interrupt
handling.
* MHCCIF (Modem Host Cross-Core Interface) - Provides
interrupt channels for bidirectional event
notification such as handshake and port enumeration.
* Transaction layer - Implements data transactions for
the control plane and the data plane. It includes:
* DPMAIF (Data Plane Modem AP Interface) - Controls
the hardware that provides uplink and downlink
queues for the data path. The data exchange takes
place using circular buffers to share data buffer
addresses and metadata to describe the packets.
* CLDMA (Cross Layer DMA) - Manages the hardware
used by the port layer to send control messages to
the device using MediaTek's CCCI (Cross-Core
Communication Interface) protocol.
* TX Services - Dispatch packets from the port layer
to the device.
* RX Services - Dispatch packets to the port layer
when receiving packets from the device.
* Port layer - Provides control plane and data plane
interfaces to userspace. It includes:
* Control Plane - Provides device node interfaces
for controlling data transactions.
* Data Plane - Provides network link interfaces
wwanX (0, 1, 2...) for IP data transactions.
* Core logic - Contains the core logic to keep the
device working. It includes:
* FSM (Finite State Machine) - Monitors the state
of the device, and notifies each module when the
state changes.
The compilation of the T9XX driver is enabled by the
CONFIG_MTK_T9XX and CONFIG_MTK_T9XX_PCI config option
which depends on CONFIG_WWAN.
This v2 submission covers the control plane only
(patches 1-6). The data plane will follow in a
separate series once the control plane is accepted.
---
Changes in v6:
- Patch 3 (Add control DMA interface):
- Move extern declarations for mtk_cldma_regs_m9xx and
cldma_drv_ops_m9xx from Patch 5 to Patch 3 where the
symbols are first defined, fixing sparse W=1 C=1 warnings
- Fix RX done recycle path: skip buffer recycle in BD mode
(nr_bds > 0) to prevent BDP flag mismatch causing HW to
misinterpret data buffer address as BD chain pointer
[sashiko]
- Fix RX done recycle path: on dma_map_single failure,
free skb and advance free_idx instead of goto out, to
prevent ring stall [sashiko]
- Patch 4 (Add control port):
- Replace wake_up_interruptible_all() with wake_up_all()
for trb_wq and rx_wq in open/close/tx completion
callbacks and common_close, fixing mismatch with
wait_event_timeout (TASK_UNINTERRUPTIBLE) waiters in
ch_enable/ch_disable that caused spurious timeouts
[sashiko]
- Link to v5: https://patch.msgid.link/20260723-t9xx_driver_v1-v5-0-b27afb99ccbb@compal.com
Changes in v5:
- Patch 1 (Add PCIe core):
- Remove LE32_TO_U32(cpu_to_le32()) no-op endianness
roundtrip, return hw_bits directly
- Use "%s" format string in pci_request_irq() to prevent
format string injection
- Return PCI_ERS_RESULT_DISCONNECT from AER error_detected,
driver does not support AER recovery
- Patch 2 (Add control plane transaction layer):
- Rewrite commit message to accurately describe skeleton
content
- Patch 3 (Add control DMA interface):
- Replace rmb() with dma_rmb() after HWO check in TX/RX
done paths for correct DMA descriptor ordering
- Add ring boundary check in TX done to prevent crossing
into submit territory
- Fix RX stall on reload failure: recycle old buffer with
skb_trim() + dma_map_single() instead of losing descriptor
- Convert devm_kcalloc to kcalloc for req_pool and
bd_dsc_pool, matching existing kfree in teardown
- Set bd_dsc->skb = NULL after dev_kfree_skb_any() in
reload error path to prevent use-after-free
- Use skb_headlen(skb) instead of skb->len for linear
segment size in txbuf_set non-BD path
- Use WRITE_ONCE() for wr_idx update in submit_tx
- Patch 4 (Add control port):
- Fix direct mtk_port_release() calls to use kref_put()
in mtk_port_free_or_backup and stale_list_grp_cleanup
- Add ida_free() before kfree(s_list) in stale list
cleanup to prevent IDA leak
- Remove double-free of skb in mtk_port_internal_recv()
drop_data path, caller owns the skb
- Add port_cnt bounds check against data_len in
mtk_port_status_update() to prevent OOB read
- Replace wait_event_interruptible_timeout with
wait_event_timeout in ch_enable/ch_disable to prevent
busy-spin on pending signals
- Patch 5 (Add FSM thread):
- Validate rtft data_len before passing to query_rtft
callback to prevent OOB access
- Zero-initialize HS3 skb data with memset() to prevent
heap infoleak
- Set hs_info->rt_data = NULL after dev_kfree_skb() in
SAP/MD ctrl msg handlers to prevent use-after-free
- Move wake_up_process() inside spinlock in evt_submit,
clear fsm_handler under lock in exit to fix race
- Replace kcalloc + radix_tree_gang_lookup with
radix_tree_for_each_slot() in mtk_port_disable()
- Add rt_data cleanup loop in FSM teardown path
- Patch 7 (Add maintainers entry):
- Add Robert Yu and Jeff Chang as maintainers
- Link to v4: https://patch.msgid.link/20260709-t9xx_driver_v1-v4-0-a8c009d509c5@compal.com
Changes in v4:
- Patch 1 (Add PCIe core):
- Add ACPI dependency to Kconfig (depends on PCI && ACPI)
and remove #ifdef CONFIG_ACPI / #else guards from
mtk_pci_fldr() and mtk_pci_pldr()
- Remove unnecessary parentheses in (mdev)->dev across
all dev_err/dev_warn calls
- Remove devm_kfree() calls from mtk_pci_probe() error
path and mtk_pci_remove(), devres handles cleanup
- Remove mtk_dev_alloc()/mtk_dev_free() wrappers, use
devm_kzalloc() directly in mtk_pci_probe()
- Convert ext_evt callbacks from devm_kzalloc/devm_kfree
to kzalloc/kfree for runtime-managed resources
- Patch 2 (Add control plane transaction layer):
- Remove mtk_dev_alloc()/mtk_dev_free() declarations and
implementations from mtk_dev.h and mtk_dev.c
- Remove empty module_init/module_exit stubs, defer to
Patch 4 where they have actual content
- Remove devm_kfree() and unused ctrl_blk variable in
mtk_ctrl_exit(), devres handles cleanup
- Patch 3 (Add control DMA interface):
- Remove inline keyword from mtk_cldma_clr_bd_dsc() in
.c file, let compiler decide inlining
- Replace dev_warn() with dev_warn_ratelimited() for SKB
alloc/map failure messages in data path
- Replace open-coded do/while polling loop with
read_poll_timeout() from <linux/iopoll.h> in
mtk_cldma_stop_queue()
- Remove ctrl_port_chl_mtu module_param, MODULE_PARM_DESC,
and mtk_ctrl_queue_info_update() with associated macros
- Remove unnecessary parentheses in (mdev)->dev across
mtk_cldma.c and mtk_trans_ctrl.c
- Convert CLDMA txq/rxq/cd from devm_kzalloc/devm_kfree
to kzalloc/kfree for runtime-managed resources
- Convert trb_srv/srv_que from devm_kzalloc/devm_kfree
to kzalloc/kfree for runtime-managed resources
- Remove devm_kfree() and unused variables from
mtk_cldma_exit() and mtk_trans_ctrl_exit(), devres
handles cleanup for device-lifetime resources
- Remove unnecessary NULL checks before kfree() for
bd_dsc_pool in txq_free/rxq_free
- Patch 4 (Add control port):
- Add module_init/module_exit with mtk_port_io_init/exit
content, moved from Patch 2
- Remove unnecessary parentheses in (mdev)->dev in
mtk_port.c
- Remove devm_kfree() from mtk_port_mngr_init() error
path and mtk_port_mngr_exit(), devres handles cleanup
- Simplify mtk_trans_ctrl_init() error path: replace goto
err_free_trans with direct return -ENOMEM
- Patch 5 (Add FSM thread):
- Remove unnecessary parentheses in (mdev)->dev across
mtk_fsm.c and mtk_ctrl_plane.c
- Convert FSM notifiers from devm_kzalloc/devm_kfree to
kzalloc/kfree for runtime-managed resources
- Convert CLDMA drv_info from devm_kzalloc/devm_kfree to
kzalloc/kfree for runtime-managed resources
- Remove devm_kfree() from mtk_fsm_init()/mtk_fsm_exit()
and mtk_ctrl_init()/mtk_ctrl_exit(), devres handles
cleanup for device-lifetime resources
- Link to v3: https://patch.msgid.link/20260624-t9xx_driver_v1-v3-0-73ff03f60c48@compal.com
Changes in v3:
- Address sashiko AI code review comments and fix sparse warnings
- Patch 1 (Add PCIe core):
- Move extern declaration of mtk_dev_cfg_0900 from mtk_pci.c to mtk_pci.h to fix sparse warning
- Remove mtk_pci_bar_exit(): pcim_iounmap_region() was called with bitmask instead of BAR index, and pcim_iomap_regions() is devres-managed so manual unmap is redundant [sashiko]
- Remove pci_disable_device() from probe error path and remove path: pcim_enable_device() registers devres cleanup, manual disable causes enable_cnt underflow [sashiko]
- Patch 3 (Add control DMA interface):
- Add #include "mtk_cldma.h" in mtk_cldma_drv_m9xx.c to fix sparse undeclared symbol warnings for mtk_cldma_regs_m9xx and cldma_drv_ops_m9xx
- Move extern declaration of mtk_ctrl_info_m9xx from mtk_trans_ctrl.c to mtk_trans_ctrl.h to fix sparse undeclared symbol warning
- Replace kcalloc + radix_tree_gang_lookup with radix_tree_for_each_slot() in mtk_ctrl_remove_radix_tree() to eliminate allocation in teardown path [sashiko]
- Add missing mtk_pci_dev_exit() call in mtk_pci_remove() to properly clean up FSM and trans_ctrl resources before device removal [sashiko]
- Patch 4 (Add control port):
- Replace kcalloc + radix_tree_gang_lookup with radix_tree_for_each_slot() and single-entry gang_lookup in mtk_port_tbl_destroy() to eliminate allocation failure in teardown path [sashiko]
- Add port = NULL after mtk_port_put_locked() in mtk_port_internal_open() error path to prevent returning un-refcounted pointer [sashiko]
- Restore skb_unlink + trb_complete for non-EAGAIN errors in mtk_ctrl_trb_handler() TX path to prevent infinite retry loop [sashiko]
- Patch 5 (Add FSM thread):
- Check mtk_pci_register_irq() return value in mtk_cldma_dev_init() and add err_destroy_wq error label to fix unreachable dead code [sashiko]
- Propagate specific error codes (ENOMEM/EIO/EINVAL) from mtk_cldma_dev_init() error paths instead of generic -EIO [sashiko]
- Free head SKB after detaching frag_list in mtk_cldma_rxq_free() scatter-gather mode to fix memory leak [sashiko]
- Patch 6 (Add AT & MBIM WWAN ports):
- Remove unused write_lock mutex from struct mtk_port and its mutex_init call [sashiko]
- Link to v2: https://patch.msgid.link/20260610-t9xx_driver_v1-v2-0-c65addf23b3f@compal.com
Changes in v2:
- Split series into control plane (this v2) and data plane (follow-up)
- Patch 1 (Add PCIe core):
- Rename BAR_NUM to MTK_PCI_BAR_NUM for driver prefix consistency
- Replace magic numbers in mtk_pci_setup_atr() with named defines
- Remove redundant ATR register comments, use blank line separators
- Add kernel-doc comments to all non-static functions
- Convert 4 MMIO wrapper functions to static inline in header [sashiko]
- Remove unnecessary unlikely() from IRQ validation paths
- Add irq_cnt == 0 and irq_id < 0 guards in mtk_pci_get_virq_id() [sashiko]
- Initialize hw_bits at declaration for consistency
- Merge same-type variable declarations into single lines
- Add #else/#endif comments for CONFIG_ACPI blocks
- Add newlines in mtk_pci_pldr() for readability
- Move return into default case in mtk_pci_dev_reset()
- Simplify mtk_mhccif_init() error path to use direct returns
- Change -EFAULT to -ENOLINK for PCIe link check failure
- Rename goto label "out" to "log_err" in mtk_pci_probe()
- Wrap long lines to stay within 80 columns
- Fix IRQ vector leak: add pci_free_irq_vectors() on error path [sashiko]
- Fix mtk_pci_remove() ordering: free IRQ before cancel_work_sync [sashiko]
- Fix mtk_pci_pldr() ACPI buffer leak: free first result before second call [sashiko]
- Replace msleep(500) with MTK_PLDR_POWER_OFF_DELAY_MS define
- Remove unused EXT_EVT_H2D_DRM_DISABLE_AP and related register define [sashiko]
- Increase MTK_IRQ_NAME_LEN from 20 to 32 to fix W=1 format-truncation warning [sashiko]
- Patch 2 (Add control plane transaction layer):
- Add kernel-doc comments to mtk_ctrl_init() and mtk_ctrl_exit()
- Change mtk_ctrl_exit() return type from int to void
- Set mdev->ctrl_blk to NULL after freeing in mtk_ctrl_exit() [sashiko]
- Change ctrl_blk from void* to typed struct mtk_ctrl_blk* [sashiko]
- Remove redundant "depends on MTK_T9XX" from MTK_T9XX_PCI Kconfig [sashiko]
- Use mtk_dev_free() instead of devm_kfree() in mtk_pci_probe() error path [sashiko]
- Patch 3 (Add control DMA interface):
- Add @ops kernel-doc parameter for mtk_ctrl_init()
- Rename 'err' to 'ret' consistently throughout the patch
- Reorder variable declarations to follow reverse Christmas tree style
- Change mtk_cldma_txq_free() return type from int to void
- Change mtk_cldma_rxq_free() return type from int to void
- Change mtk_cldma_exit() return type from int to void
- Remove unnecessary zero-initialization of ret in mtk_cldma_start_xfer()
- Remove unnecessary zero-initialization of ret in mtk_cldma_tx()
- Use direct return instead of goto out in mtk_cldma_submit_tx() error paths
- Move software state before HWO flag in mtk_cldma_submit_tx()
- Squash variable declarations in mtk_cldma_check_intr_status()
- Remove unlikely() from validation paths in mtk_cldma_check_ch_cfg()
- Clamp data_recv_len with min_t to prevent skb_over_panic in mtk_cldma_rx_skb_adjust() [sashiko]
- Use READ_ONCE() for HWO flag polling in mtk_cldma_check_rx_req() [sashiko]
- Fix mtk_cldma_rx_done_work() to always unmask interrupt on error path [sashiko]
- Add DMA address guard in mtk_cldma_txq_free() teardown loop [sashiko]
- Add IS_ERR() check for kthread_run() in mtk_ctrl_trb_srv_init() [sashiko]
- Fix queue_info memory leak on validation failure in mtk_pcie_hif_init() [sashiko]
- Handle non-EAGAIN errors in mtk_ctrl_trb_handler() TX path [sashiko]
- Fix 'err' typo to 'ret' in mtk_cldma_txbuf_set() error message
- Remove unused variable mdev in mtk_cldma_rx_check_again() [sashiko]
- Remove unused variables trans and ctrl_blk in mtk_cldma_txq_free() and mtk_cldma_rxq_free() [sashiko]
- Patch 4 (Add control port):
- Add @cfg kernel-doc parameter for mtk_ctrl_init()
- Update mtk_ctrl_init() return description to cover additional error codes
- Fix double list_del in mtk_port_stale_list_grp_cleanup() [sashiko]
- Fix direct mtk_port_trb_free() call to use kref_put() in mtk_port_ch_enable() error path [sashiko]
- Fix direct mtk_port_trb_free() call to use kref_put() in mtk_port_ch_disable() error path [sashiko]
- Add mtk_port_tbl_destroy() in mtk_port_mngr_init() error path to prevent port memory leak [sashiko]
- Change port_ops exit/reset/enable/disable callbacks from int to void
- Move -EIO dispatch comment to where the code was introduced
- Patch 5 (Add FSM thread):
- Add bounds check for rtft_entry in mtk_fsm_parse_hs2_msg() [sashiko]
- Add skb length validation before accessing ctrl_msg_header in mtk_fsm_sap_ctrl_msg_handler() [sashiko]
- Fix skb leak on CTRL_MSG_HS2 mismatch return in mtk_fsm_sap_ctrl_msg_handler() [sashiko]
- Add skb length validation before accessing ctrl_msg_header in mtk_fsm_md_ctrl_msg_handler() [sashiko]
- Replace devm_kzalloc/devm_kfree with kzalloc/kfree for FSM events [sashiko]
- Fix mtk_fsm_evt_submit() to return -ETIMEDOUT on blocking event timeout [sashiko]
- Change FSM kthread from TASK_INTERRUPTIBLE to TASK_UNINTERRUPTIBLE [sashiko]
- Remove unused variable hw_id in mtk_cldma_dev_exit() [sashiko]
- Patch 6 (Add AT & MBIM WWAN ports):
- Use imperative mode in commit message
- Remove unnecessary zero-initialization of ret in mtk_port_copy_data_from()
- Change copy_from_user() error code from -EFAULT to -EINVAL in mtk_port_copy_data_from()
- Return -EINVAL for zero-length write in mtk_port_common_write()
- Change mtk_port_wwan_exit/enable/disable() return type from int to void
- Fix packet_size to account for CCCI header reservation in mtk_port_common_write() [sashiko]
- Fix WWAN tx callbacks to consume skb and return 0 per wwan_port_ops contract [sashiko]
- Fix wwan_create_port() error path: clear ERR_PTR to NULL and call mtk_port_ch_disable() [sashiko]
- Patch 7 (Add maintainers entry): new patch
- Link to v1: https://patch.msgid.link/20260529-t9xx_driver_v1-v1-0-bdbfe2c01e57@compal.com
To: Loic Poulain <loic.poulain@oss.qualcomm.com>
To: Sergey Ryazanov <ryazanov.s.a@gmail.com>
To: Johannes Berg <johannes@sipsolutions.net>
To: Andrew Lunn <andrew+netdev@lunn.ch>
To: "David S. Miller" <davem@davemloft.net>
To: Eric Dumazet <edumazet@google.com>
To: Jakub Kicinski <kuba@kernel.org>
To: Paolo Abeni <pabeni@redhat.com>
To: Jack Wu <jackbb_wu@compal.com>
To: Robert Yu <robert_yu@compal.com>
To: Jeff Chang <Jeff_Chang@compal.com>
To: Wen-Zhi Huang <wen-zhi.huang@mediatek.com>
To: Shi-Wei Yeh <shi-wei.yeh@mediatek.com>
To: Minano Tseng <Minano.tseng@mediatek.com>
To: Matthias Brugger <matthias.bgg@gmail.com>
To: AngeloGioacchino Del Regno <angelogioacchino.delregno@collabora.com>
Cc: linux-kernel@vger.kernel.org
Cc: netdev@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org
Cc: linux-mediatek@lists.infradead.org
---
Jack Wu (7):
net: wwan: t9xx: Add PCIe core
net: wwan: t9xx: Add control plane transaction layer
net: wwan: t9xx: Add control DMA interface
net: wwan: t9xx: Add control port
net: wwan: t9xx: Add FSM thread
net: wwan: t9xx: Add AT & MBIM WWAN ports
net: wwan: t9xx: Add maintainers entry
MAINTAINERS | 11 +
drivers/net/wwan/Kconfig | 17 +
drivers/net/wwan/Makefile | 1 +
drivers/net/wwan/t9xx/Makefile | 14 +
drivers/net/wwan/t9xx/mtk_ctrl_plane.c | 108 ++
drivers/net/wwan/t9xx/mtk_ctrl_plane.h | 88 ++
drivers/net/wwan/t9xx/mtk_dev.c | 33 +
drivers/net/wwan/t9xx/mtk_dev.h | 111 ++
drivers/net/wwan/t9xx/mtk_fsm.c | 965 +++++++++++++++
drivers/net/wwan/t9xx/mtk_fsm.h | 140 +++
drivers/net/wwan/t9xx/mtk_port.c | 964 +++++++++++++++
drivers/net/wwan/t9xx/mtk_port.h | 174 +++
drivers/net/wwan/t9xx/mtk_port_io.c | 573 +++++++++
drivers/net/wwan/t9xx/mtk_port_io.h | 41 +
drivers/net/wwan/t9xx/mtk_utility.h | 33 +
drivers/net/wwan/t9xx/pcie/Makefile | 15 +
drivers/net/wwan/t9xx/pcie/mtk_cldma.c | 1452 +++++++++++++++++++++++
drivers/net/wwan/t9xx/pcie/mtk_cldma.h | 173 +++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c | 369 ++++++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h | 174 +++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c | 178 +++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h | 101 ++
drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c | 55 +
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 1083 +++++++++++++++++
drivers/net/wwan/t9xx/pcie/mtk_pci.h | 234 ++++
drivers/net/wwan/t9xx/pcie/mtk_pci_drv_m9xx.c | 69 ++
drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h | 71 ++
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c | 563 +++++++++
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h | 107 ++
29 files changed, 7917 insertions(+)
---
base-commit: eb3f4b7426cfd2b79d65b7d37155480b32259a11
change-id: 20260529-t9xx_driver_v1-1744f8af7739
Best regards,
--
Jack Wu <jackbb_wu@compal.com>
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:35 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
` (6 subsequent siblings)
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
Registers the T900 device driver with the kernel. Set up all
the fundamental configurations for the device: PCIe layer,
Modem Host Cross Core Interface (MHCCIF), Reset Generation
Unit (RGU), modem common control operations and build
infrastructure.
* PCIe layer code implements driver probe and removal, MSI-X
interrupt initialization and de-initialization, and the way
of resetting the device.
* MHCCIF provides interrupt channels to communicate events
such as handshake, PM and port enumeration.
* RGU provides interrupt channels to generate notifications
from the device so that the T900 driver could get the
device reset.
* Modem common control operations provide the basic read/write
functions of the device's hardware registers,
mask/unmask/get/clear functions of the device's interrupt
registers and inquiry functions of the device's status.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/Kconfig | 12 +
drivers/net/wwan/Makefile | 1 +
drivers/net/wwan/t9xx/Makefile | 10 +
drivers/net/wwan/t9xx/mtk_dev.h | 108 +++
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 1029 +++++++++++++++++++++++++
drivers/net/wwan/t9xx/pcie/mtk_pci.h | 234 ++++++
drivers/net/wwan/t9xx/pcie/mtk_pci_drv_m9xx.c | 69 ++
drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h | 70 ++
8 files changed, 1533 insertions(+)
diff --git a/drivers/net/wwan/Kconfig b/drivers/net/wwan/Kconfig
index 88df55d78d90..18bd40b3b8d2 100644
--- a/drivers/net/wwan/Kconfig
+++ b/drivers/net/wwan/Kconfig
@@ -121,6 +121,18 @@ config MTK_T7XX
If unsure, say N.
+config MTK_T9XX
+ tristate "MediaTek PCIe 5G WWAN modem T9xx device"
+ depends on PCI && ACPI
+ select NET_DEVLINK
+ help
+ Enables MediaTek PCIe based 5G WWAN modem (T9xx series) device.
+
+ To compile this driver as a module, choose M here: the module will be
+ called mtk_t9xx.
+
+ If unsure, say N.
+
endif # WWAN
endmenu
diff --git a/drivers/net/wwan/Makefile b/drivers/net/wwan/Makefile
index 3960c0ae2445..7361eef4c472 100644
--- a/drivers/net/wwan/Makefile
+++ b/drivers/net/wwan/Makefile
@@ -14,3 +14,4 @@ obj-$(CONFIG_QCOM_BAM_DMUX) += qcom_bam_dmux.o
obj-$(CONFIG_RPMSG_WWAN_CTRL) += rpmsg_wwan_ctrl.o
obj-$(CONFIG_IOSM) += iosm/
obj-$(CONFIG_MTK_T7XX) += t7xx/
+obj-$(CONFIG_MTK_T9XX) += t9xx/
diff --git a/drivers/net/wwan/t9xx/Makefile b/drivers/net/wwan/t9xx/Makefile
new file mode 100644
index 000000000000..6f2dd3f91454
--- /dev/null
+++ b/drivers/net/wwan/t9xx/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+ccflags-y += -I$(src)/pcie
+ccflags-y += -I$(src)
+
+obj-$(CONFIG_MTK_T9XX) += mtk_t9xx.o
+
+mtk_t9xx-y := \
+ pcie/mtk_pci.o \
+ pcie/mtk_pci_drv_m9xx.o
diff --git a/drivers/net/wwan/t9xx/mtk_dev.h b/drivers/net/wwan/t9xx/mtk_dev.h
new file mode 100644
index 000000000000..8278a0e2875e
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_dev.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_DEV_H__
+#define __MTK_DEV_H__
+
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+#define MTK_DEV_STR_LEN 16
+
+enum mtk_user_id {
+ MTK_USER_MIN,
+ MTK_USER_CTRL,
+ MTK_USER_DATA,
+ MTK_USER_MAX
+};
+
+enum mtk_dev_evt_h2d {
+ DEV_EVT_H2D_DEVICE_RESET = BIT(2),
+ DEV_EVT_H2D_MAX = BIT(5)
+};
+
+enum mtk_dev_evt_d2h {
+ DEV_EVT_D2H_BOOT_FLOW_SYNC = BIT(4),
+ DEV_EVT_D2H_ASYNC_HS_NOTIFY_SAP = BIT(5),
+ DEV_EVT_D2H_ASYNC_HS_NOTIFY_MD = BIT(6),
+ DEV_EVT_D2H_MAX = BIT(11)
+};
+
+struct mtk_md_dev;
+
+struct mtk_dev_ops {
+ u32 (*get_dev_state)(struct mtk_md_dev *mdev);
+ void (*ack_dev_state)(struct mtk_md_dev *mdev, u32 state);
+ u32 (*get_dev_cfg)(struct mtk_md_dev *mdev);
+ int (*register_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt,
+ int (*evt_cb)(u32 status, void *data), void *data);
+ void (*unregister_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt);
+ void (*mask_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt);
+ void (*unmask_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt);
+ void (*clear_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt);
+ int (*send_dev_evt)(struct mtk_md_dev *mdev, u32 dev_evt);
+};
+
+/* mtk_md_dev defines the structure of MTK modem device */
+struct mtk_md_dev {
+ struct device *dev;
+ const struct mtk_dev_ops *dev_ops;
+ void *hw_priv;
+ u32 hw_ver;
+ char dev_str[MTK_DEV_STR_LEN];
+};
+
+static inline u32 mtk_dev_get_dev_state(struct mtk_md_dev *mdev)
+{
+ return mdev->dev_ops->get_dev_state(mdev);
+}
+
+static inline void mtk_dev_ack_dev_state(struct mtk_md_dev *mdev, u32 state)
+{
+ return mdev->dev_ops->ack_dev_state(mdev, state);
+}
+
+static inline u32 mtk_dev_get_dev_cfg(struct mtk_md_dev *mdev)
+{
+ return mdev->dev_ops->get_dev_cfg(mdev);
+}
+
+static inline int mtk_dev_register_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt,
+ int (*evt_cb)(u32 status, void *data), void *data)
+{
+ return mdev->dev_ops->register_dev_evt(mdev, dev_evt, evt_cb, data);
+}
+
+static inline void mtk_dev_unregister_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt)
+{
+ mdev->dev_ops->unregister_dev_evt(mdev, dev_evt);
+}
+
+static inline void mtk_dev_mask_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt)
+{
+ mdev->dev_ops->mask_dev_evt(mdev, dev_evt);
+}
+
+static inline void mtk_dev_unmask_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt)
+{
+ mdev->dev_ops->unmask_dev_evt(mdev, dev_evt);
+}
+
+static inline void mtk_dev_clear_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt)
+{
+ mdev->dev_ops->clear_dev_evt(mdev, dev_evt);
+}
+
+static inline int mtk_dev_send_dev_evt(struct mtk_md_dev *mdev, u32 dev_evt)
+{
+ return mdev->dev_ops->send_dev_evt(mdev, dev_evt);
+}
+
+#endif /* __MTK_DEV_H__ */
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
new file mode 100644
index 000000000000..1f6cca70b74c
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
@@ -0,0 +1,1029 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/acpi.h>
+#include <linux/aer.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+
+#include "mtk_dev.h"
+#include "mtk_pci.h"
+#include "mtk_pci_reg.h"
+
+#define MTK_PCI_BAR_NUM 6
+#define MTK_PCI_TRANSPARENT_ATR_SIZE (0x3F)
+#define MTK_PCI_MINIMUM_ATR_SIZE (0x1000)
+#define ATR_SIZE_LO32_MASK GENMASK_ULL(31, 0)
+#define ATR_SIZE_HI32_MASK GENMASK_ULL(63, 32)
+#define ATR_SIZE_BIAS_FROM_LO32 2
+#define ATR_ADDR_ALIGN_MASK 0xFFFFF000
+#define ATR_EN BIT(0)
+#define ATR_PARAM_OFFSET 16
+/* Delay between ACPI PXP._OFF and _ON for modem power cycle stabilization */
+#define MTK_PLDR_POWER_OFF_DELAY_MS 500
+#define SET_HW_BITS(dest, chs, mhccif, dev) \
+ ({ \
+ if ((chs) & (dev)) \
+ (dest) |= FIELD_PREP(mhccif, 1); \
+ })
+
+struct mtk_mhccif_cb {
+ struct list_head entry;
+ int (*evt_cb)(u32 status, void *data);
+ void *data;
+ u32 chs;
+};
+
+/**
+ * mtk_pci_setup_atr() - Configure a PCIe address translation rule
+ * @mdev: MTK MD device
+ * @cfg: ATR configuration parameters
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_setup_atr(struct mtk_md_dev *mdev, struct mtk_atr_cfg *cfg)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 addr, val, size_h, size_l;
+ int atr_size, pos, offset;
+
+ if (cfg->transparent) {
+ /* No address conversion is performed */
+ atr_size = MTK_PCI_TRANSPARENT_ATR_SIZE;
+ } else {
+ if (cfg->size < MTK_PCI_MINIMUM_ATR_SIZE)
+ cfg->size = MTK_PCI_MINIMUM_ATR_SIZE;
+
+ if (cfg->src_addr & (cfg->size - 1)) {
+ dev_err(mdev->dev, "Invalid atr src addr is not aligned to size\n");
+ return -EFAULT;
+ }
+
+ if (cfg->trsl_addr & (cfg->size - 1)) {
+ dev_err(mdev->dev,
+ "Invalid atr trsl addr is not aligned to size, %llx, %llx\n",
+ cfg->trsl_addr, cfg->size - 1);
+ return -EFAULT;
+ }
+
+ size_l = FIELD_GET(ATR_SIZE_LO32_MASK, cfg->size);
+ size_h = FIELD_GET(ATR_SIZE_HI32_MASK, cfg->size);
+ pos = ffs(size_l);
+ if (pos) {
+ atr_size = pos - ATR_SIZE_BIAS_FROM_LO32;
+ } else {
+ pos = ffs(size_h);
+ atr_size = pos + 32 - ATR_SIZE_BIAS_FROM_LO32;
+ }
+ }
+
+ /* Calculate table offset */
+ offset = ATR_PORT_OFFSET * cfg->port + ATR_TABLE_OFFSET * cfg->table;
+ addr = REG_ATR_PCIE_WIN0_T0_SRC_ADDR_MSB + offset;
+ val = (u32)(cfg->src_addr >> 32);
+ mtk_pci_mac_write32(priv, addr, val);
+
+ addr = REG_ATR_PCIE_WIN0_T0_SRC_ADDR_LSB + offset;
+ val = (u32)(cfg->src_addr & ATR_ADDR_ALIGN_MASK) | (atr_size << 1) | ATR_EN;
+ mtk_pci_mac_write32(priv, addr, val);
+
+ addr = REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_MSB + offset;
+ val = (u32)(cfg->trsl_addr >> 32);
+ mtk_pci_mac_write32(priv, addr, val);
+
+ addr = REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_LSB + offset;
+ val = (u32)(cfg->trsl_addr & ATR_ADDR_ALIGN_MASK);
+ mtk_pci_mac_write32(priv, addr, val);
+
+ /* TRSL_PARAM */
+ addr = REG_ATR_PCIE_WIN0_T0_TRSL_PARAM + offset;
+ val = (cfg->trsl_param << ATR_PARAM_OFFSET) | cfg->trsl_id;
+ mtk_pci_mac_write32(priv, addr, val);
+
+ return 0;
+}
+
+/**
+ * mtk_pci_atr_disable() - Disable all PCIe address translation rules
+ * @priv: MTK PCI private data
+ */
+void mtk_pci_atr_disable(struct mtk_pci_priv *priv)
+{
+ int port, tbl, offset;
+ u32 val;
+
+ /* Disable all ATR table for all ports */
+ for (port = ATR_SRC_PCI_WIN0; port <= ATR_SRC_AXIS_3; port++)
+ for (tbl = 0; tbl < ATR_TABLE_NUM_PER_ATR; tbl++) {
+ /* Calculate table offset */
+ offset = ATR_PORT_OFFSET * port + ATR_TABLE_OFFSET * tbl;
+ val = mtk_pci_mac_read32(priv, REG_ATR_PCIE_WIN0_T0_SRC_ADDR_LSB + offset);
+ val = val & (~BIT(0));
+ /* Disable table by SRC_ADDR_L */
+ mtk_pci_mac_write32(priv, REG_ATR_PCIE_WIN0_T0_SRC_ADDR_LSB + offset, val);
+ }
+}
+
+static void mtk_pci_set_msix_merged(struct mtk_pci_priv *priv, int irq_cnt)
+{
+ mtk_pci_mac_write32(priv, REG_PCIE_CFG_MSIX, ffs(irq_cnt) * 2 - 1);
+}
+
+/**
+ * mtk_pci_get_dev_state() - Read the device state from the modem
+ * @mdev: MTK MD device
+ *
+ * Return: Device state value.
+ */
+u32 mtk_pci_get_dev_state(struct mtk_md_dev *mdev)
+{
+ return mtk_pci_mac_read32(mdev->hw_priv, REG_PCIE_DEBUG_DUMMY_7);
+}
+
+/**
+ * mtk_pci_ack_dev_state() - Acknowledge the device state to the modem
+ * @mdev: MTK MD device
+ * @state: State value to acknowledge
+ */
+void mtk_pci_ack_dev_state(struct mtk_md_dev *mdev, u32 state)
+{
+ mtk_pci_mac_write32(mdev->hw_priv, REG_PCIE_DEBUG_DUMMY_7, state);
+}
+
+/**
+ * mtk_pci_get_irq_id() - Map an IRQ source to its hardware IRQ ID
+ * @mdev: MTK MD device
+ * @irq_src: IRQ source enum
+ *
+ * Return: IRQ ID on success, -EINVAL on failure.
+ */
+int mtk_pci_get_irq_id(struct mtk_md_dev *mdev, enum mtk_irq_src irq_src)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ const int *irq_tbl = priv->cfg->irq_tbl;
+ int irq_id = -EINVAL;
+
+ if (irq_src > MTK_IRQ_SRC_MIN && irq_src < MTK_IRQ_SRC_MAX) {
+ irq_id = irq_tbl[irq_src];
+ if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX)
+ irq_id = -EINVAL;
+ }
+
+ return irq_id;
+}
+
+/**
+ * mtk_pci_get_virq_id() - Get the Linux virtual IRQ for a hardware IRQ ID
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ *
+ * Return: Virtual IRQ number on success, negative error code on failure.
+ */
+int mtk_pci_get_virq_id(struct mtk_md_dev *mdev, int irq_id)
+{
+ struct pci_dev *pdev = to_pci_dev(mdev->dev);
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if (!priv->irq_cnt || irq_id < 0)
+ return -EINVAL;
+
+ return pci_irq_vector(pdev, irq_id % priv->irq_cnt);
+}
+
+/**
+ * mtk_pci_register_irq() - Register a callback for a hardware IRQ
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ * @irq_cb: Callback function
+ * @data: Private data passed to callback
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_register_irq(struct mtk_md_dev *mdev, int irq_id,
+ int (*irq_cb)(int irq_id, void *data), void *data)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if ((irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX) || !irq_cb)
+ return -EINVAL;
+
+ if (priv->irq_cb_list[irq_id]) {
+ dev_err(mdev->dev,
+ "Unable to register irq, irq_id=%d, it's already been register by %ps.\n",
+ irq_id, priv->irq_cb_list[irq_id]);
+ return -EFAULT;
+ }
+ priv->irq_cb_list[irq_id] = irq_cb;
+ priv->irq_cb_data[irq_id] = data;
+
+ return 0;
+}
+
+/**
+ * mtk_pci_unregister_irq() - Unregister a hardware IRQ callback
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_unregister_irq(struct mtk_md_dev *mdev, int irq_id)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX)
+ return -EINVAL;
+
+ if (!priv->irq_cb_list[irq_id]) {
+ dev_err(mdev->dev, "irq_id=%d has not been registered\n", irq_id);
+ return -EFAULT;
+ }
+ priv->irq_cb_list[irq_id] = NULL;
+ priv->irq_cb_data[irq_id] = NULL;
+
+ return 0;
+}
+
+/**
+ * mtk_pci_mask_irq() - Mask (disable) a hardware IRQ
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_mask_irq(struct mtk_md_dev *mdev, int irq_id)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX ||
+ priv->irq_type != PCI_IRQ_MSIX) {
+ dev_err(mdev->dev, "Failed to mask irq: input irq_id=%d\n", irq_id);
+ return -EINVAL;
+ }
+
+ mtk_pci_mac_write32(priv, REG_IMASK_HOST_MSIX_CLR_GRP0_0, BIT(irq_id));
+
+ return 0;
+}
+
+/**
+ * mtk_pci_unmask_irq() - Unmask (enable) a hardware IRQ
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_unmask_irq(struct mtk_md_dev *mdev, int irq_id)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX ||
+ priv->irq_type != PCI_IRQ_MSIX) {
+ dev_err(mdev->dev, "Failed to unmask irq: input irq_id=%d\n", irq_id);
+ return -EINVAL;
+ }
+
+ mtk_pci_mac_write32(priv, REG_IMASK_HOST_MSIX_SET_GRP0_0, BIT(irq_id));
+
+ return 0;
+}
+
+/**
+ * mtk_pci_clear_irq() - Clear (acknowledge) a hardware IRQ
+ * @mdev: MTK MD device
+ * @irq_id: Hardware IRQ ID
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_clear_irq(struct mtk_md_dev *mdev, int irq_id)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX ||
+ priv->irq_type != PCI_IRQ_MSIX) {
+ dev_err(mdev->dev, "Failed to clear irq: input irq_id=%d\n", irq_id);
+ return -EINVAL;
+ }
+
+ mtk_pci_mac_write32(priv, REG_MSIX_ISTATUS_HOST_GRP0_0, BIT(irq_id));
+
+ return 0;
+}
+
+static u32 mtk_pci_ext_d2h_evt_hw_bits(u32 chs)
+{
+ u32 hw_bits = 0;
+
+ SET_HW_BITS(hw_bits, chs, MHCCIF_EP2RC_EVT_BOOT_FLOW_SYNC,
+ DEV_EVT_D2H_BOOT_FLOW_SYNC);
+ SET_HW_BITS(hw_bits, chs, MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_SAP,
+ DEV_EVT_D2H_ASYNC_HS_NOTIFY_SAP);
+ SET_HW_BITS(hw_bits, chs, MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_MD,
+ DEV_EVT_D2H_ASYNC_HS_NOTIFY_MD);
+
+ return hw_bits;
+}
+
+static u32 mtk_pci_ext_d2h_evt_chs(u32 hw_bits)
+{
+ u32 chs = 0;
+
+ if (!hw_bits)
+ return chs;
+
+ chs = FIELD_PREP(DEV_EVT_D2H_BOOT_FLOW_SYNC,
+ FIELD_GET(MHCCIF_EP2RC_EVT_BOOT_FLOW_SYNC, hw_bits)) |
+ FIELD_PREP(DEV_EVT_D2H_ASYNC_HS_NOTIFY_SAP,
+ FIELD_GET(MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_SAP, hw_bits)) |
+ FIELD_PREP(DEV_EVT_D2H_ASYNC_HS_NOTIFY_MD,
+ FIELD_GET(MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_MD, hw_bits));
+
+ return chs;
+}
+
+/**
+ * mtk_pci_register_ext_evt() - Register a callback for MHCCIF device events
+ * @mdev: MTK MD device
+ * @chs: Bitmask of event channels to register
+ * @evt_cb: Callback function
+ * @data: Private data passed to callback
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_register_ext_evt(struct mtk_md_dev *mdev, u32 chs,
+ int (*evt_cb)(u32 status, void *data), void *data)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ struct mtk_mhccif_cb *cb;
+ int ret = 0;
+
+ if (!chs || !evt_cb)
+ return -EINVAL;
+
+ spin_lock_bh(&priv->mhccif_lock);
+ list_for_each_entry(cb, &priv->mhccif_cb_list, entry) {
+ if (cb->chs & chs) {
+ ret = -EFAULT;
+ dev_err(mdev->dev,
+ "Unable to register evt, intersection: chs=0x%08x&0x%08x cb=%ps\n",
+ chs, cb->chs, cb->evt_cb);
+ goto err_spin_unlock;
+ }
+ }
+ cb = kzalloc(sizeof(*cb), GFP_ATOMIC);
+ if (!cb) {
+ ret = -ENOMEM;
+ goto err_spin_unlock;
+ }
+ cb->evt_cb = evt_cb;
+ cb->data = data;
+ cb->chs = chs;
+ list_add_tail(&cb->entry, &priv->mhccif_cb_list);
+err_spin_unlock:
+ spin_unlock_bh(&priv->mhccif_lock);
+
+ return ret;
+}
+
+/**
+ * mtk_pci_unregister_ext_evt() - Unregister an MHCCIF device event callback
+ * @mdev: MTK MD device
+ * @chs: Bitmask of event channels to unregister
+ */
+void mtk_pci_unregister_ext_evt(struct mtk_md_dev *mdev, u32 chs)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ struct mtk_mhccif_cb *cb, *next;
+
+ if (!chs)
+ return;
+
+ spin_lock_bh(&priv->mhccif_lock);
+ list_for_each_entry_safe(cb, next, &priv->mhccif_cb_list, entry) {
+ if (cb->chs == chs) {
+ list_del(&cb->entry);
+ kfree(cb);
+ goto out;
+ }
+ }
+ dev_warn(mdev->dev,
+ "Unable to unregister evt, no chs=0x%08x has been registered.\n", chs);
+out:
+ spin_unlock_bh(&priv->mhccif_lock);
+}
+
+/**
+ * mtk_pci_mask_ext_evt() - Mask (disable) MHCCIF device events
+ * @mdev: MTK MD device
+ * @chs: Bitmask of event channels to mask
+ */
+void mtk_pci_mask_ext_evt(struct mtk_md_dev *mdev, u32 chs)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 hw_bits = mtk_pci_ext_d2h_evt_hw_bits(chs);
+
+ mtk_pci_write32(mdev, priv->cfg->mhccif_rc_base_addr +
+ MHCCIF_EP2RC_SW_INT_EAP_MASK_SET, hw_bits);
+}
+
+/**
+ * mtk_pci_unmask_ext_evt() - Unmask (enable) MHCCIF device events
+ * @mdev: MTK MD device
+ * @chs: Bitmask of event channels to unmask
+ */
+void mtk_pci_unmask_ext_evt(struct mtk_md_dev *mdev, u32 chs)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 hw_bits = mtk_pci_ext_d2h_evt_hw_bits(chs);
+
+ mtk_pci_write32(mdev, priv->cfg->mhccif_rc_base_addr +
+ MHCCIF_EP2RC_SW_INT_EAP_MASK_CLR, hw_bits);
+}
+
+/**
+ * mtk_pci_clear_ext_evt() - Clear (acknowledge) MHCCIF device events
+ * @mdev: MTK MD device
+ * @chs: Bitmask of event channels to clear
+ */
+void mtk_pci_clear_ext_evt(struct mtk_md_dev *mdev, u32 chs)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 hw_bits = mtk_pci_ext_d2h_evt_hw_bits(chs);
+
+ mtk_pci_write32(mdev, priv->cfg->mhccif_rc_base_addr +
+ MHCCIF_EP2RC_SW_INT_ACK, hw_bits);
+}
+
+static u32 mtk_pci_ext_h2d_evt_hw_bits(u32 chs)
+{
+ u32 hw_bits = 0;
+
+ SET_HW_BITS(hw_bits, chs, MHCCIF_RC2EP_EVT_DEVICE_RESET,
+ DEV_EVT_H2D_DEVICE_RESET);
+ return hw_bits;
+}
+
+/**
+ * mtk_pci_send_ext_evt() - Send an MHCCIF event to the modem
+ * @mdev: MTK MD device
+ * @ch: Event channel to trigger (must be a single bit)
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_send_ext_evt(struct mtk_md_dev *mdev, u32 ch)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 rc_base, hw_bits;
+
+ rc_base = priv->cfg->mhccif_rc_base_addr;
+
+ /* Only allow one ch to be triggered at a time */
+ if (!is_power_of_2(ch)) {
+ dev_err(mdev->dev, "Unsupported ext evt ch=0x%08x\n", ch);
+ return -EINVAL;
+ }
+
+ hw_bits = mtk_pci_ext_h2d_evt_hw_bits(ch);
+ mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_BSY, hw_bits);
+ mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_TCHNUM, ffs(hw_bits) - 1);
+ return 0;
+}
+
+static u32 mtk_pci_get_ext_evt_hw_status(struct mtk_md_dev *mdev)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ return mtk_pci_read32(mdev, priv->cfg->mhccif_rc_base_addr +
+ MHCCIF_EP2RC_SW_INT_STS);
+}
+
+/**
+ * mtk_pci_fldr() - Perform a Function Level Device Reset via ACPI _RST
+ * @mdev: MTK MD device
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_fldr(struct mtk_md_dev *mdev)
+{
+ struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
+ acpi_status acpi_ret;
+ acpi_handle handle;
+
+ if (acpi_disabled) {
+ dev_err(mdev->dev, "Unsupported, acpi function isn't enable\n");
+ return -ENODEV;
+ }
+
+ handle = ACPI_HANDLE(mdev->dev);
+
+ if (!handle) {
+ dev_err(mdev->dev, "Unsupported, acpi handle isn't found\n");
+ return -ENODEV;
+ }
+
+ if (!acpi_has_method(handle, "_RST")) {
+ dev_err(mdev->dev, "Unsupported, _RST method isn't found\n");
+ return -ENODEV;
+ }
+
+ acpi_ret = acpi_evaluate_object(handle, "_RST", NULL, &buffer);
+ if (ACPI_FAILURE(acpi_ret)) {
+ dev_err(mdev->dev, "Failed to execute _RST method: %s\n",
+ acpi_format_exception(acpi_ret));
+ return -EFAULT;
+ }
+
+ acpi_os_free(buffer.pointer);
+
+ return 0;
+}
+
+/**
+ * mtk_pci_pldr() - Perform a PCIe Link Down Reset via ACPI PXP._OFF/_ON
+ * @mdev: MTK MD device
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_pldr(struct mtk_md_dev *mdev)
+{
+ struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
+ struct pci_dev *bridge;
+ acpi_status acpi_ret;
+ acpi_handle handle;
+
+ if (acpi_disabled) {
+ dev_err(mdev->dev, "Unsupported, acpi function isn't enable\n");
+ return -ENODEV;
+ }
+
+ bridge = pci_upstream_bridge(to_pci_dev(mdev->dev));
+ if (!bridge) {
+ dev_err(mdev->dev, "Unable to find bridge\n");
+ return -ENODEV;
+ }
+
+ handle = ACPI_HANDLE(&bridge->dev);
+ if (!handle) {
+ dev_err(mdev->dev, "Unsupported, acpi handle isn't found\n");
+ return -ENODEV;
+ }
+ if (!acpi_has_method(handle, "PXP._OFF") ||
+ !acpi_has_method(handle, "PXP._ON")) {
+ dev_err(mdev->dev, "Unsupported, pldr method isn't supported\n");
+ return -ENODEV;
+ }
+ acpi_ret = acpi_evaluate_object(handle, "PXP._OFF", NULL, &buffer);
+ if (ACPI_FAILURE(acpi_ret)) {
+ dev_err(mdev->dev, "Failed to execute _OFF method: %s\n",
+ acpi_format_exception(acpi_ret));
+ return -EFAULT;
+ }
+ acpi_os_free(buffer.pointer);
+
+ msleep(MTK_PLDR_POWER_OFF_DELAY_MS);
+
+ buffer.length = ACPI_ALLOCATE_BUFFER;
+ buffer.pointer = NULL;
+ acpi_ret = acpi_evaluate_object(handle, "PXP._ON", NULL, &buffer);
+ if (ACPI_FAILURE(acpi_ret)) {
+ dev_err(mdev->dev, "Failed to execute _ON method: %s\n",
+ acpi_format_exception(acpi_ret));
+ return -EFAULT;
+ }
+ acpi_os_free(buffer.pointer);
+
+ return 0;
+}
+
+/**
+ * mtk_pci_get_dev_cfg() - Read the device configuration from the modem
+ * @mdev: MTK MD device
+ *
+ * Return: Device configuration value.
+ */
+u32 mtk_pci_get_dev_cfg(struct mtk_md_dev *mdev)
+{
+ u32 val;
+
+ val = mtk_pci_mac_read32(mdev->hw_priv, REG_PCIE_DEBUG_DUMMY_4);
+ return (val >> MTK_CFG_INFO_BIT_SHIFT);
+}
+
+static int mtk_pci_dev_reset(struct mtk_md_dev *mdev, enum mtk_reset_type type)
+{
+ switch (type) {
+ case RESET_MHCCIF:
+ return mtk_pci_send_ext_evt(mdev, DEV_EVT_H2D_DEVICE_RESET);
+ case RESET_FLDR:
+ return mtk_pci_fldr(mdev);
+ case RESET_PLDR:
+ return mtk_pci_pldr(mdev);
+ default:
+ return -EINVAL;
+ }
+}
+
+/**
+ * mtk_pci_reset() - Reset the modem device
+ * @mdev: MTK MD device
+ * @type: Reset type (MHCCIF, FLDR, or PLDR)
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int mtk_pci_reset(struct mtk_md_dev *mdev, enum mtk_reset_type type)
+{
+ return mtk_pci_dev_reset(mdev, type);
+}
+
+/**
+ * mtk_pci_link_check() - Check if the PCIe link to the modem is active
+ * @mdev: MTK MD device
+ *
+ * Return: true if the device is present, false otherwise.
+ */
+bool mtk_pci_link_check(struct mtk_md_dev *mdev)
+{
+ return pci_device_is_present(to_pci_dev(mdev->dev));
+}
+
+static void mtk_mhccif_isr_work(struct work_struct *work)
+{
+ struct mtk_pci_priv *priv =
+ container_of(work, struct mtk_pci_priv, mhccif_work);
+ struct mtk_md_dev *mdev = priv->irq_desc->mdev;
+ struct mtk_mhccif_cb *cb;
+ u32 stat, mask, chs;
+
+ stat = mtk_pci_get_ext_evt_hw_status(mdev);
+ mask = mtk_pci_read32(mdev, priv->cfg->mhccif_rc_base_addr
+ + MHCCIF_EP2RC_SW_INT_EAP_MASK);
+ if (unlikely(stat == U32_MAX && !(mtk_pci_link_check(mdev)))) {
+ /* When link failed, we don't need to unmask/clear. */
+ dev_err(mdev->dev, "Failed to check link in MHCCIF handler.\n");
+ return;
+ }
+
+ stat &= ~mask;
+ chs = mtk_pci_ext_d2h_evt_chs(stat);
+ spin_lock_bh(&priv->mhccif_lock);
+ list_for_each_entry(cb, &priv->mhccif_cb_list, entry) {
+ if (cb->chs & chs)
+ cb->evt_cb(cb->chs & chs, cb->data);
+ }
+ spin_unlock_bh(&priv->mhccif_lock);
+
+ mtk_pci_clear_irq(mdev, priv->mhccif_irq_id);
+ mtk_pci_unmask_irq(mdev, priv->mhccif_irq_id);
+}
+
+static const struct pci_device_id t9xx_pci_table[] = {
+ MTK_PCI_DEV_CFG(0x0900, mtk_dev_cfg_0900),
+ CEI_PCI_DEV_CFG(0x01CA, mtk_dev_cfg_0900),
+ {/* end: all zeroes */}
+};
+
+MODULE_DEVICE_TABLE(pci, t9xx_pci_table);
+
+static int mtk_pci_bar_init(struct mtk_md_dev *mdev)
+{
+ struct pci_dev *pdev = to_pci_dev(mdev->dev);
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ u32 bar[MTK_PCI_BAR_NUM];
+ int i, ret;
+
+ for (i = 0; i < MTK_PCI_BAR_NUM; i++)
+ pci_read_config_dword(to_pci_dev(mdev->dev),
+ PCI_BASE_ADDRESS_0 + (i << 2), bar + i);
+
+ ret = pcim_iomap_regions(pdev, MTK_REQUESTED_BARS, mdev->dev_str);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to init MMIO. ret=%d\n", ret);
+ return ret;
+ }
+
+ /* get ioremapped memory */
+ priv->mac_reg_base = pcim_iomap_table(pdev)[MTK_BAR_0_1_IDX];
+ priv->bar23_addr = pcim_iomap_table(pdev)[MTK_BAR_2_3_IDX];
+ if (!priv->mac_reg_base || !priv->bar23_addr) {
+ dev_err(mdev->dev, "Failed to init BAR.\n");
+ return -EINVAL;
+ }
+ /* We use MD view base address "0" to observe registers */
+ priv->ext_reg_base = priv->bar23_addr - ATR_PCIE_REG_TRSL_ADDR;
+
+ return 0;
+}
+
+static int mtk_mhccif_irq_cb(int irq_id, void *data)
+{
+ struct mtk_md_dev *mdev = data;
+ struct mtk_pci_priv *priv;
+
+ priv = mdev->hw_priv;
+ queue_work(system_highpri_wq, &priv->mhccif_work);
+
+ return 0;
+}
+
+static int mtk_mhccif_init(struct mtk_md_dev *mdev)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ int ret;
+
+ INIT_LIST_HEAD(&priv->mhccif_cb_list);
+ spin_lock_init(&priv->mhccif_lock);
+ INIT_WORK(&priv->mhccif_work, mtk_mhccif_isr_work);
+
+ ret = mtk_pci_get_irq_id(mdev, MTK_IRQ_SRC_MHCCIF);
+ if (ret < 0) {
+ dev_err(mdev->dev, "Failed to get mhccif_irq_id. ret=%d\n", ret);
+ return ret;
+ }
+ priv->mhccif_irq_id = ret;
+
+ ret = mtk_pci_register_irq(mdev, priv->mhccif_irq_id, mtk_mhccif_irq_cb, mdev);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to register mhccif_irq callback\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static void mtk_mhccif_exit(struct mtk_md_dev *mdev)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+
+ mtk_pci_unregister_irq(mdev, priv->mhccif_irq_id);
+ cancel_work_sync(&priv->mhccif_work);
+}
+
+static irqreturn_t mtk_pci_irq_handler(struct mtk_md_dev *mdev, u32 irq_state)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ int irq_id;
+
+ /* Check whether each set bit has a callback, if has, call it */
+ do {
+ irq_id = fls(irq_state) - 1;
+ irq_state &= ~BIT(irq_id);
+ if (likely(priv->irq_cb_list[irq_id]))
+ priv->irq_cb_list[irq_id](irq_id, priv->irq_cb_data[irq_id]);
+ else
+ dev_err(mdev->dev, "Unhandled irq_id=%d, no callback for it.\n", irq_id);
+ } while (irq_state);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t mtk_pci_irq_msix(int irq, void *data)
+{
+ struct mtk_pci_irq_desc *irq_desc = data;
+ struct mtk_md_dev *mdev = irq_desc->mdev;
+ struct mtk_pci_priv *priv;
+ u32 irq_state, irq_enable;
+
+ priv = mdev->hw_priv;
+ irq_state = mtk_pci_mac_read32(priv, REG_MSIX_ISTATUS_HOST_GRP0_0);
+ irq_enable = mtk_pci_mac_read32(priv, REG_IMASK_HOST_MSIX_GRP0_0);
+ irq_state &= irq_enable;
+
+ if (unlikely(!irq_state) ||
+ unlikely(!((irq_state & GENMASK(priv->irq_cnt - 1, 0)) &
+ irq_desc->msix_bits)))
+ return IRQ_NONE;
+
+ /* Mask the bit and user needs to unmask by itself */
+ mtk_pci_mac_write32(priv, REG_IMASK_HOST_MSIX_CLR_GRP0_0,
+ irq_state & ~BIT(30));
+
+ return mtk_pci_irq_handler(mdev, irq_state);
+}
+
+static int mtk_pci_request_irq_msix(struct mtk_md_dev *mdev,
+ int irq_cnt_allocated)
+{
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ struct mtk_pci_irq_desc *irq_desc;
+ struct pci_dev *pdev;
+ int irq_cnt;
+ int ret, i;
+
+ /* calculate the nearest 2's power number */
+ irq_cnt = BIT(fls(irq_cnt_allocated) - 1);
+ pdev = to_pci_dev(mdev->dev);
+ irq_desc = priv->irq_desc;
+ for (i = 0; i < irq_cnt; i++) {
+ irq_desc[i].mdev = mdev;
+ irq_desc[i].msix_bits = BIT(i);
+ snprintf(irq_desc[i].name, MTK_IRQ_NAME_LEN, "msix%d-%s", i, mdev->dev_str);
+ ret = pci_request_irq(pdev, i, mtk_pci_irq_msix, NULL,
+ &irq_desc[i], "%s", irq_desc[i].name);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to request %s: ret=%d\n",
+ irq_desc[i].name, ret);
+ for (i--; i >= 0; i--)
+ pci_free_irq(pdev, i, &irq_desc[i]);
+ return ret;
+ }
+ }
+ priv->irq_cnt = irq_cnt;
+ priv->irq_type = PCI_IRQ_MSIX;
+
+ if (irq_cnt != MTK_IRQ_CNT_MAX)
+ mtk_pci_set_msix_merged(priv, irq_cnt);
+
+ return 0;
+}
+
+static int mtk_pci_request_irq(struct mtk_md_dev *mdev)
+{
+ struct pci_dev *pdev = to_pci_dev(mdev->dev);
+ int irq_cnt, ret;
+
+ irq_cnt = pci_alloc_irq_vectors(pdev, MTK_IRQ_CNT_MIN,
+ MTK_IRQ_CNT_MAX, PCI_IRQ_MSIX);
+
+ if (irq_cnt < MTK_IRQ_CNT_MIN) {
+ dev_err(mdev->dev,
+ "Unable to alloc pci irq vectors. ret=%d maxirqcnt=%d irqtype=0x%x\n",
+ irq_cnt, MTK_IRQ_CNT_MAX, PCI_IRQ_MSIX);
+ return -EFAULT;
+ }
+
+ ret = mtk_pci_request_irq_msix(mdev, irq_cnt);
+ if (ret)
+ pci_free_irq_vectors(pdev);
+
+ return ret;
+}
+
+static void mtk_pci_free_irq(struct mtk_md_dev *mdev)
+{
+ struct pci_dev *pdev = to_pci_dev(mdev->dev);
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ int i;
+
+ for (i = 0; i < priv->irq_cnt; i++)
+ pci_free_irq(pdev, i, &priv->irq_desc[i]);
+
+ pci_free_irq_vectors(pdev);
+}
+
+static const struct mtk_dev_ops pci_hw_ops = {
+ .get_dev_state = mtk_pci_get_dev_state,
+ .ack_dev_state = mtk_pci_ack_dev_state,
+ .get_dev_cfg = mtk_pci_get_dev_cfg,
+ .register_dev_evt = mtk_pci_register_ext_evt,
+ .unregister_dev_evt = mtk_pci_unregister_ext_evt,
+ .mask_dev_evt = mtk_pci_mask_ext_evt,
+ .unmask_dev_evt = mtk_pci_unmask_ext_evt,
+ .clear_dev_evt = mtk_pci_clear_ext_evt,
+ .send_dev_evt = mtk_pci_send_ext_evt,
+};
+
+static int mtk_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+{
+ struct device *dev = &pdev->dev;
+ struct mtk_pci_priv *priv;
+ struct mtk_md_dev *mdev;
+ int ret;
+
+ mdev = devm_kzalloc(dev, sizeof(*mdev), GFP_KERNEL);
+ if (!mdev) {
+ ret = -ENOMEM;
+ goto log_err;
+ }
+ mdev->dev_ops = &pci_hw_ops;
+ mdev->dev = dev;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv) {
+ ret = -ENOMEM;
+ goto log_err;
+ }
+
+ pci_set_drvdata(pdev, mdev);
+ priv->cfg = (void *)id->driver_data;
+ priv->mdev = mdev;
+ mdev->hw_ver = pdev->device;
+ mdev->hw_priv = priv;
+ mdev->dev = dev;
+ snprintf(mdev->dev_str, MTK_DEV_STR_LEN, "%02x%02x%d",
+ pdev->bus->number, PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn));
+ if (pdev->state_saved)
+ pci_restore_state(pdev);
+
+ ret = pcim_enable_device(pdev);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to enable pci device.\n");
+ goto log_err;
+ }
+
+ ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
+ if (ret) {
+ dev_err(mdev->dev, "Failed to set DMA Mask and Coherent. (ret=%d)\n", ret);
+ goto log_err;
+ }
+
+ ret = mtk_pci_bar_init(mdev);
+ if (ret)
+ goto log_err;
+
+ ret = priv->cfg->atr_init(mdev);
+ if (ret)
+ goto log_err;
+
+ ret = mtk_mhccif_init(mdev);
+ if (ret)
+ goto log_err;
+
+ /* mask all irqs */
+ if (priv->cfg->flag & MTK_CFG_IRQ_DFLT_MASK)
+ mtk_pci_mac_write32(priv, REG_IMASK_HOST_MSIX_CLR_GRP0_0, U32_MAX);
+
+ ret = mtk_pci_request_irq(mdev);
+ if (ret)
+ goto free_mhccif;
+
+ pci_set_master(pdev);
+ mtk_pci_unmask_irq(mdev, priv->mhccif_irq_id);
+
+ if (mtk_pci_link_check(mdev)) {
+ pci_save_state(pdev);
+ } else {
+ ret = -ENOLINK;
+ goto clear_master;
+ }
+
+ priv->saved_state = pci_store_saved_state(pdev);
+ if (!priv->saved_state) {
+ ret = -EFAULT;
+ goto clear_master;
+ }
+
+ return 0;
+
+clear_master:
+ pci_clear_master(pdev);
+ mtk_pci_free_irq(mdev);
+free_mhccif:
+ mtk_mhccif_exit(mdev);
+log_err:
+ dev_err(dev, "Failed to probe device, ret=%d\n", ret);
+
+ return ret;
+}
+
+static void mtk_pci_remove(struct pci_dev *pdev)
+{
+ struct mtk_md_dev *mdev = pci_get_drvdata(pdev);
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ struct device *dev = &pdev->dev;
+
+ mtk_pci_mask_irq(mdev, priv->mhccif_irq_id);
+
+ if (mtk_pci_pldr(mdev)) {
+ dev_warn(dev, "Failed to execute PLDR, try external event\n");
+ mtk_pci_reset(mdev, RESET_MHCCIF);
+ }
+
+ pci_clear_master(pdev);
+ mtk_pci_free_irq(mdev);
+ mtk_mhccif_exit(mdev);
+ pci_load_and_free_saved_state(pdev, &priv->saved_state);
+}
+
+static pci_ers_result_t mtk_pci_error_detected(struct pci_dev *pdev,
+ pci_channel_state_t state)
+{
+ struct mtk_md_dev *mdev = pci_get_drvdata(pdev);
+
+ dev_err(mdev->dev, "AER detected: pci_channel_state_t=%d\n", state);
+
+ /* AER recovery not supported, disconnect the device */
+ return PCI_ERS_RESULT_DISCONNECT;
+}
+
+static const struct pci_error_handlers mtk_pci_err_handler = {
+ .error_detected = mtk_pci_error_detected,
+};
+
+static struct pci_driver mtk_pci_drv = {
+ .name = "mtk_pci_drv",
+ .id_table = t9xx_pci_table,
+ .probe = mtk_pci_probe,
+ .remove = mtk_pci_remove,
+ .err_handler = &mtk_pci_err_handler
+};
+
+module_pci_driver(mtk_pci_drv);
+
+MODULE_DESCRIPTION("MediaTek T9xx PCIe WWAN driver pcie layer");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.h b/drivers/net/wwan/t9xx/pcie/mtk_pci.h
new file mode 100644
index 000000000000..9819a1b07c1b
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.h
@@ -0,0 +1,234 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_PCI_H__
+#define __MTK_PCI_H__
+
+#include <linux/pci.h>
+
+#include "../mtk_dev.h"
+
+enum mtk_irq_src {
+ MTK_IRQ_SRC_MIN,
+ MTK_IRQ_SRC_MHCCIF,
+ MTK_IRQ_SRC_DPMAIF,
+ MTK_IRQ_SRC_DPMAIF2,
+ MTK_IRQ_SRC_CLDMA0,
+ MTK_IRQ_SRC_CLDMA1,
+ MTK_IRQ_SRC_CLDMA2,
+ MTK_IRQ_SRC_CLDMA3,
+ MTK_IRQ_SRC_PM_LOCK,
+ MTK_IRQ_SRC_DPMAIF3,
+ MTK_IRQ_SRC_DPMAIF6,
+ MTK_IRQ_SRC_MAX
+};
+
+enum mtk_reset_type {
+ RESET_FLDR,
+ RESET_PLDR,
+ RESET_MHCCIF,
+};
+
+enum mtk_atr_type {
+ ATR_PCI2AXI = 0,
+ ATR_AXI2PCI,
+};
+
+enum mtk_atr_src_port {
+ ATR_SRC_PCI_WIN0 = 0,
+ ATR_SRC_PCI_WIN1,
+ ATR_SRC_AXIS_0,
+ ATR_SRC_AXIS_1,
+ ATR_SRC_AXIS_2,
+ ATR_SRC_AXIS_3,
+};
+
+enum mtk_atr_dst_port {
+ ATR_DST_PCI_TRX = 0,
+ ATR_DST_AXIM_0 = 4,
+ ATR_DST_AXIM_1,
+ ATR_DST_AXIM_2,
+ ATR_DST_AXIM_3,
+};
+
+enum mtk_pci_evt_h2d {
+ DEV_EVT_H2D_EXTEND_BASE = DEV_EVT_H2D_MAX,
+ EXT_EVT_H2D_RESERVED_FOR_CLDMA0 = DEV_EVT_H2D_EXTEND_BASE << 1,
+ EXT_EVT_H2D_RESERVED_FOR_CLDMA1 = DEV_EVT_H2D_EXTEND_BASE << 2,
+ EXT_EVT_H2D_RESERVED_FOR_CLDMA3 = DEV_EVT_H2D_EXTEND_BASE << 3,
+ EXT_EVT_H2D_RESERVED_FOR_CLDMA2 = DEV_EVT_H2D_EXTEND_BASE << 4,
+ EXT_EVT_H2D_RESERVED_FOR_DPMAIF = DEV_EVT_H2D_EXTEND_BASE << 5,
+ EXT_EVT_H2D_PCIE_PM_SUSPEND_REQ = DEV_EVT_H2D_EXTEND_BASE << 6,
+ EXT_EVT_H2D_PCIE_PM_RESUME_REQ = DEV_EVT_H2D_EXTEND_BASE << 7,
+ EXT_EVT_H2D_PCIE_PM_SUSPEND_REQ_AP = DEV_EVT_H2D_EXTEND_BASE << 8,
+ EXT_EVT_H2D_PCIE_PM_RESUME_REQ_AP = DEV_EVT_H2D_EXTEND_BASE << 9,
+ EXT_EVT_H2D_RESERVED_FOR_TEST = DEV_EVT_H2D_EXTEND_BASE << 11,
+};
+
+enum mtk_pci_evt_d2h {
+ DEV_EVT_D2H_EXTEND_BASE = DEV_EVT_D2H_MAX,
+ EXT_EVT_D2H_RESERVED_FOR_CLDMA0 = DEV_EVT_D2H_EXTEND_BASE << 1,
+ EXT_EVT_D2H_RESERVED_FOR_CLDMA1 = DEV_EVT_D2H_EXTEND_BASE << 2,
+ EXT_EVT_D2H_RESERVED_FOR_CLDMA3 = DEV_EVT_D2H_EXTEND_BASE << 3,
+ EXT_EVT_D2H_RESERVED_FOR_CLDMA2 = DEV_EVT_D2H_EXTEND_BASE << 4,
+ EXT_EVT_D2H_RESERVED_FOR_DPMAIF = DEV_EVT_D2H_EXTEND_BASE << 5,
+ EXT_EVT_D2H_PCIE_PM_SUSPEND_ACK = DEV_EVT_D2H_EXTEND_BASE << 6,
+ EXT_EVT_D2H_PCIE_PM_RESUME_ACK = DEV_EVT_D2H_EXTEND_BASE << 7,
+ EXT_EVT_D2H_PCIE_PM_SUSPEND_ACK_AP = DEV_EVT_D2H_EXTEND_BASE << 8,
+ EXT_EVT_D2H_PCIE_PM_RESUME_ACK_AP = DEV_EVT_D2H_EXTEND_BASE << 9,
+ EXT_EVT_D2H_SOFT_OFF_NOTIFY = DEV_EVT_D2H_EXTEND_BASE << 10,
+ EXT_EVT_D2H_FRC_DONE_NOTIFY = DEV_EVT_D2H_EXTEND_BASE << 11,
+ EXT_EVT_D2H_RESERVED_FOR_TEST1 = DEV_EVT_D2H_EXTEND_BASE << 12,
+ EXT_EVT_D2H_RESERVED_FOR_TEST2 = DEV_EVT_D2H_EXTEND_BASE << 13,
+};
+
+#define MTK_PCI_CLASS 0x0D4000
+#define MTK_PCI_VENDOR_ID 0x14C3
+#define CEI_PCI_VENDOR_ID 0x03F0
+
+#define MTK_CFG_INFO_BIT_SHIFT 4
+
+#define MTK_PCI_DEV_CFG(id, cfg) \
+{ \
+ PCI_DEVICE(MTK_PCI_VENDOR_ID, id), \
+ MTK_PCI_CLASS, PCI_ANY_ID, \
+ .driver_data = (kernel_ulong_t)&(cfg), \
+}
+
+#define CEI_PCI_DEV_CFG(id, cfg) \
+{ \
+ PCI_DEVICE(CEI_PCI_VENDOR_ID, id), \
+ MTK_PCI_CLASS, PCI_ANY_ID, \
+ .driver_data = (kernel_ulong_t)&(cfg), \
+}
+
+#define MTK_CFG_IRQ_DFLT_MASK BIT(0)
+#define MTK_CFG_DISABLE_AP_DRM BIT(2)
+#define MTK_CFG_PM_SW_IRQ BIT(6)
+
+#define MTK_BAR_0_1_IDX 0
+#define MTK_BAR_2_3_IDX 2
+
+#define MTK_REQUESTED_BARS \
+ ((1 << MTK_BAR_0_1_IDX) | \
+ (1 << MTK_BAR_2_3_IDX))
+
+#define MTK_IRQ_CNT_MIN 1
+#define MTK_IRQ_CNT_MAX 32
+#define MTK_IRQ_NAME_LEN 32
+
+#define ATR_PORT_OFFSET 0x100
+#define ATR_TABLE_OFFSET 0x20
+#define ATR_TABLE_NUM_PER_ATR 8
+#define ATR_PCIE_REG_TRSL_ADDR 0x10000000
+#define ATR_PCIE_REG_SIZE 0x00400000
+#define ATR_PCIE_REG_PORT ATR_SRC_PCI_WIN0
+#define ATR_PCIE_REG_TABLE_NUM 1
+#define ATR_PCIE_REG_TRSL_PORT ATR_DST_AXIM_0
+#define ATR_PCIE_DEV_DMA_SRC_ADDR 0x00000000
+#define ATR_PCIE_DEV_DMA_TRANSPARENT 1
+#define ATR_PCIE_DEV_DMA_SIZE 0
+#define ATR_PCIE_DEV_DMA_TABLE_NUM 0
+#define ATR_PCIE_DEV_DMA_TRSL_ADDR 0x00000000
+
+struct mtk_pci_irq_desc {
+ struct mtk_md_dev *mdev;
+ u32 msix_bits;
+ char name[MTK_IRQ_NAME_LEN];
+};
+
+struct mtk_pci_dev_cfg {
+ u32 flag;
+ u32 mhccif_rc_base_addr;
+ u32 istatus_host_ctrl_addr;
+ int irq_tbl[MTK_IRQ_SRC_MAX];
+ int (*atr_init)(struct mtk_md_dev *mdev);
+};
+
+extern const struct mtk_pci_dev_cfg mtk_dev_cfg_0900;
+
+struct mtk_pci_priv {
+ struct mtk_md_dev *mdev;
+ const struct mtk_pci_dev_cfg *cfg;
+ void __iomem *bar23_addr;
+ void __iomem *mac_reg_base;
+ void __iomem *ext_reg_base;
+ int irq_cnt;
+ int irq_type;
+ void *irq_cb_data[MTK_IRQ_CNT_MAX];
+
+ int (*irq_cb_list[MTK_IRQ_CNT_MAX])(int irq_id, void *data);
+ struct mtk_pci_irq_desc irq_desc[MTK_IRQ_CNT_MAX];
+ struct list_head mhccif_cb_list;
+ /* mhccif_lock: lock to protect mhccif_cb_list */
+ spinlock_t mhccif_lock;
+ struct work_struct mhccif_work;
+ int mhccif_irq_id;
+ struct pci_saved_state *saved_state;
+};
+
+struct mtk_atr_cfg {
+ u64 src_addr;
+ u64 trsl_addr;
+ u64 size;
+ u32 type; /* Port type */
+ u32 port; /* Port number */
+ u32 table; /* Table number (8 tables for each port) */
+ u32 trsl_id;
+ u32 trsl_param;
+ u32 transparent;
+};
+
+/* BAR 0/1 MMIO access */
+static inline u32 mtk_pci_mac_read32(struct mtk_pci_priv *priv, u64 addr)
+{
+ return ioread32(priv->mac_reg_base + addr);
+}
+
+static inline void mtk_pci_mac_write32(struct mtk_pci_priv *priv, u64 addr, u32 val)
+{
+ iowrite32(val, priv->mac_reg_base + addr);
+}
+
+/* BAR 2/3 MMIO access */
+static inline u32 mtk_pci_read32(struct mtk_md_dev *mdev, u64 addr)
+{
+ return ioread32(((struct mtk_pci_priv *)mdev->hw_priv)->ext_reg_base + addr);
+}
+
+static inline void mtk_pci_write32(struct mtk_md_dev *mdev, u64 addr, u32 val)
+{
+ iowrite32(val, ((struct mtk_pci_priv *)mdev->hw_priv)->ext_reg_base + addr);
+}
+
+/* Device operations */
+u32 mtk_pci_get_dev_state(struct mtk_md_dev *mdev);
+void mtk_pci_ack_dev_state(struct mtk_md_dev *mdev, u32 state);
+u32 mtk_pci_get_dev_cfg(struct mtk_md_dev *mdev);
+/* IRQ Related operations */
+int mtk_pci_get_irq_id(struct mtk_md_dev *mdev, enum mtk_irq_src irq_src);
+int mtk_pci_get_virq_id(struct mtk_md_dev *mdev, int irq_id);
+int mtk_pci_register_irq(struct mtk_md_dev *mdev, int irq_id,
+ int (*irq_cb)(int irq_id, void *data), void *data);
+int mtk_pci_unregister_irq(struct mtk_md_dev *mdev, int irq_id);
+int mtk_pci_mask_irq(struct mtk_md_dev *mdev, int irq_id);
+int mtk_pci_unmask_irq(struct mtk_md_dev *mdev, int irq_id);
+int mtk_pci_clear_irq(struct mtk_md_dev *mdev, int irq_id);
+/* External event related */
+int mtk_pci_register_ext_evt(struct mtk_md_dev *mdev, u32 chs,
+ int (*evt_cb)(u32 status, void *data), void *data);
+void mtk_pci_unregister_ext_evt(struct mtk_md_dev *mdev, u32 chs);
+void mtk_pci_mask_ext_evt(struct mtk_md_dev *mdev, u32 chs);
+void mtk_pci_unmask_ext_evt(struct mtk_md_dev *mdev, u32 chs);
+void mtk_pci_clear_ext_evt(struct mtk_md_dev *mdev, u32 chs);
+int mtk_pci_send_ext_evt(struct mtk_md_dev *mdev, u32 ch);
+int mtk_pci_fldr(struct mtk_md_dev *mdev);
+int mtk_pci_pldr(struct mtk_md_dev *mdev);
+int mtk_pci_reset(struct mtk_md_dev *mdev, enum mtk_reset_type type);
+bool mtk_pci_link_check(struct mtk_md_dev *mdev);
+int mtk_pci_setup_atr(struct mtk_md_dev *mdev, struct mtk_atr_cfg *cfg);
+void mtk_pci_atr_disable(struct mtk_pci_priv *priv);
+
+#endif /* __MTK_PCI_H__ */
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci_drv_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_pci_drv_m9xx.c
new file mode 100644
index 000000000000..88b44142afb7
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci_drv_m9xx.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+#include <linux/types.h>
+#include "mtk_pci.h"
+#include "mtk_pci_reg.h"
+
+static int mtk_pci_atr_init_m9xx(struct mtk_md_dev *mdev)
+{
+ struct pci_dev *pdev = to_pci_dev(mdev->dev);
+ struct mtk_pci_priv *priv = mdev->hw_priv;
+ struct mtk_atr_cfg cfg;
+ int port, ret;
+
+ mtk_pci_atr_disable(priv);
+
+ /* Config ATR for RC to access device's register */
+ cfg.src_addr = pci_resource_start(pdev, MTK_BAR_2_3_IDX);
+ cfg.size = ATR_PCIE_REG_SIZE;
+ cfg.trsl_addr = ATR_PCIE_REG_TRSL_ADDR;
+ cfg.type = ATR_PCI2AXI;
+ cfg.port = ATR_PCIE_REG_PORT;
+ cfg.table = ATR_PCIE_REG_TABLE_NUM;
+ cfg.trsl_id = ATR_PCIE_REG_TRSL_PORT;
+ cfg.trsl_param = 0x0;
+ cfg.transparent = 0x0;
+ ret = mtk_pci_setup_atr(mdev, &cfg);
+ if (ret)
+ return ret;
+
+ /* Config ATR for EP to access RC's memory */
+ for (port = ATR_SRC_AXIS_0; port <= ATR_SRC_AXIS_3; port++) {
+ cfg.src_addr = ATR_PCIE_DEV_DMA_SRC_ADDR;
+ cfg.size = ATR_PCIE_DEV_DMA_SIZE;
+ cfg.trsl_addr = ATR_PCIE_DEV_DMA_TRSL_ADDR;
+ cfg.type = ATR_AXI2PCI;
+ cfg.port = port;
+ cfg.table = ATR_PCIE_DEV_DMA_TABLE_NUM;
+ cfg.trsl_id = ATR_DST_PCI_TRX;
+ cfg.trsl_param = 0x0;
+ /* Enable transparent translation */
+ cfg.transparent = ATR_PCIE_DEV_DMA_TRANSPARENT;
+ ret = mtk_pci_setup_atr(mdev, &cfg);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+const struct mtk_pci_dev_cfg mtk_dev_cfg_0900 = {
+ .flag = MTK_CFG_PM_SW_IRQ,
+ .mhccif_rc_base_addr = 0x1000A000,
+ .istatus_host_ctrl_addr = REG_ISTATUS_HOST_CTRL_NEW,
+ .irq_tbl = {
+ [MTK_IRQ_SRC_DPMAIF] = 24,
+ [MTK_IRQ_SRC_CLDMA0] = 27,
+ [MTK_IRQ_SRC_CLDMA1] = 26,
+ [MTK_IRQ_SRC_CLDMA2] = 25,
+ [MTK_IRQ_SRC_MHCCIF] = 28,
+ [MTK_IRQ_SRC_DPMAIF2] = 29,
+ [MTK_IRQ_SRC_CLDMA3] = 31,
+ [MTK_IRQ_SRC_PM_LOCK] = 0,
+ [MTK_IRQ_SRC_DPMAIF3] = 7,
+ [MTK_IRQ_SRC_DPMAIF6] = 10,
+ },
+ .atr_init = mtk_pci_atr_init_m9xx,
+};
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h b/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h
new file mode 100644
index 000000000000..3f0667e8a846
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_PCI_REG_H__
+#define __MTK_PCI_REG_H__
+
+#define REG_ISTATUS_HOST_CTRL_NEW 0x031C
+#define REG_PCIE_MISC_CTRL 0x0348
+#define REG_PCIE_CFG_MSIX 0x03EC
+#define REG_ATR_PCIE_WIN0_T0_SRC_ADDR_LSB 0x0600
+#define REG_ATR_PCIE_WIN0_T0_SRC_ADDR_MSB 0x0604
+#define REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_LSB 0x0608
+#define REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_MSB 0x060C
+#define REG_ATR_PCIE_WIN0_T0_TRSL_PARAM 0x0610
+#define REG_PCIE_DEBUG_DUMMY_3 0x0D0C
+#define REG_PCIE_DEBUG_DUMMY_4 0x0D10
+#define REG_PCIE_DEBUG_DUMMY_7 0x0D1C
+#define REG_MSIX_ISTATUS_HOST_GRP0_0 0x0F00
+#define REG_IMASK_HOST_MSIX_SET_GRP0_0 0x3000
+#define REG_IMASK_HOST_MSIX_CLR_GRP0_0 0x3080
+#define REG_IMASK_HOST_MSIX_GRP0_0 0x3100
+
+/* mhccif registers */
+#define MHCCIF_RC2EP_SW_BSY 0x4
+#define MHCCIF_RC2EP_SW_TCHNUM 0xC
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_CLDMA0 BIT(4)
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_CLDMA1 BIT(5)
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_CLDMA3 BIT(6)
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_CLDMA2 BIT(7)
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_DPMAIF BIT(8)
+#define MHCCIF_RC2EP_EVT_PCIE_PM_SUSPEND_REQ BIT(9)
+#define MHCCIF_RC2EP_EVT_PCIE_PM_RESUME_REQ BIT(10)
+#define MHCCIF_RC2EP_EVT_PCIE_PM_SUSPEND_REQ_AP BIT(11)
+#define MHCCIF_RC2EP_EVT_PCIE_PM_RESUME_REQ_AP BIT(12)
+#define MHCCIF_RC2EP_EVT_DEVICE_RESET BIT(13)
+#define MHCCIF_RC2EP_EVT_RESERVED_FOR_TEST BIT(31)
+
+#define MHCCIF_EP2RC_SW_INT_STS 0x10
+#define MHCCIF_EP2RC_SW_INT_ACK 0x14
+#define MHCCIF_EP2RC_SW_INT_EAP_MASK 0x20
+#define MHCCIF_EP2RC_SW_INT_EAP_MASK_SET 0x30
+#define MHCCIF_EP2RC_SW_INT_EAP_MASK_CLR 0x40
+#define MHCCIF_EP2RC_SPARE_REG_1 0x0104
+#define MHCCIF_EP2RC_SPARE_REG_5 0x0114
+#define MHCCIF_EP2RC_SPARE_REG_13 0x0134
+#define MHCCIF_EP2RC_SPARE_REG_14 0x0138
+#define MHCCIF_EP2RC_EVT_BOOT_FLOW_SYNC BIT(5)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_CLDMA0 BIT(6)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_CLDMA1 BIT(7)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_CLDMA3 BIT(8)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_CLDMA2 BIT(9)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_DPMAIF BIT(10)
+#define MHCCIF_EP2RC_EVT_PCIE_PM_SUSPEND_ACK BIT(11)
+#define MHCCIF_EP2RC_EVT_PCIE_PM_RESUME_ACK BIT(12)
+#define MHCCIF_EP2RC_EVT_PCIE_PM_SUSPEND_ACK_AP BIT(13)
+#define MHCCIF_EP2RC_EVT_PCIE_PM_RESUME_ACK_AP BIT(14)
+#define MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_SAP BIT(15)
+#define MHCCIF_EP2RC_EVT_ASYNC_HS_NOTIFY_MD BIT(16)
+#define MHCCIF_EP2RC_EVT_SOFT_OFF_NOTIFY BIT(17)
+#define MHCCIF_EP2RC_EVT_MD_REBOOT BIT(19)
+#define MHCCIF_EP2RC_EVT_MD_POWEROFF BIT(20)
+#define MHCCIF_EP2RC_EVT_GNSS_ENABLE BIT(21)
+#define MHCCIF_EP2RC_EVT_GNSS_DISABLE BIT(22)
+#define MHCCIF_EP2RC_EVT_FRC_DONE_NOTIFY BIT(24)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_TEST1 BIT(30)
+#define MHCCIF_EP2RC_EVT_RESERVED_FOR_TEST2 BIT(31)
+
+#endif /* __MTK_PCI_REG_H__ */
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
` (5 subsequent siblings)
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
Introduce the control plane and transaction layer framework
for the T9XX WWAN driver. This patch adds the core data
structures (mtk_ctrl_blk, mtk_ctrl_trans) and initialization
entry points that subsequent patches build upon.
The control plane block (ctrl_blk) serves as the central
context for managing the transaction layer, port layer, and
FSM interactions. The actual DMA engine and TX/RX service
implementations are added in subsequent patches.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/Kconfig | 5 ++++
drivers/net/wwan/t9xx/Makefile | 5 ++--
drivers/net/wwan/t9xx/mtk_ctrl_plane.c | 45 +++++++++++++++++++++++++++++
drivers/net/wwan/t9xx/mtk_ctrl_plane.h | 22 ++++++++++++++
drivers/net/wwan/t9xx/mtk_dev.c | 11 +++++++
drivers/net/wwan/t9xx/mtk_dev.h | 2 ++
drivers/net/wwan/t9xx/pcie/Makefile | 10 +++++++
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 1 +
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h | 21 ++++++++++++++
9 files changed, 120 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wwan/Kconfig b/drivers/net/wwan/Kconfig
index 18bd40b3b8d2..a2ffed346acc 100644
--- a/drivers/net/wwan/Kconfig
+++ b/drivers/net/wwan/Kconfig
@@ -124,6 +124,7 @@ config MTK_T7XX
config MTK_T9XX
tristate "MediaTek PCIe 5G WWAN modem T9xx device"
depends on PCI && ACPI
+ select MTK_T9XX_PCI
select NET_DEVLINK
help
Enables MediaTek PCIe based 5G WWAN modem (T9xx series) device.
@@ -133,6 +134,10 @@ config MTK_T9XX
If unsure, say N.
+config MTK_T9XX_PCI
+ tristate
+ depends on PCI
+
endif # WWAN
endmenu
diff --git a/drivers/net/wwan/t9xx/Makefile b/drivers/net/wwan/t9xx/Makefile
index 6f2dd3f91454..ae9d6f2344ab 100644
--- a/drivers/net/wwan/t9xx/Makefile
+++ b/drivers/net/wwan/t9xx/Makefile
@@ -4,7 +4,8 @@ ccflags-y += -I$(src)/pcie
ccflags-y += -I$(src)
obj-$(CONFIG_MTK_T9XX) += mtk_t9xx.o
+obj-$(CONFIG_MTK_T9XX_PCI) += pcie/
mtk_t9xx-y := \
- pcie/mtk_pci.o \
- pcie/mtk_pci_drv_m9xx.o
+ mtk_dev.o \
+ mtk_ctrl_plane.o
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
new file mode 100644
index 000000000000..cf6079218e1c
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ * Copyright (c) 2022-2023, Intel Corporation.
+ */
+
+#include <linux/device.h>
+
+#include "mtk_ctrl_plane.h"
+
+/**
+ * mtk_ctrl_init() - Initialize the control plane block.
+ * @mdev: Pointer to the MTK modem device.
+ *
+ * Allocates and initializes the control plane block
+ * associated with @mdev.
+ *
+ * Return: 0 on success, -ENOMEM on allocation failure.
+ */
+int mtk_ctrl_init(struct mtk_md_dev *mdev)
+{
+ struct mtk_ctrl_blk *ctrl_blk;
+
+ ctrl_blk = devm_kzalloc(mdev->dev, sizeof(*ctrl_blk), GFP_KERNEL);
+ if (!ctrl_blk)
+ return -ENOMEM;
+
+ ctrl_blk->mdev = mdev;
+ mdev->ctrl_blk = ctrl_blk;
+
+ return 0;
+}
+EXPORT_SYMBOL(mtk_ctrl_init);
+
+/**
+ * mtk_ctrl_exit() - Clean up the control plane block.
+ * @mdev: Pointer to the MTK modem device.
+ *
+ * Frees the control plane block associated with @mdev.
+ */
+void mtk_ctrl_exit(struct mtk_md_dev *mdev)
+{
+ mdev->ctrl_blk = NULL;
+}
+EXPORT_SYMBOL(mtk_ctrl_exit);
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
new file mode 100644
index 000000000000..c141876ef95d
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_CTRL_PLANE_H__
+#define __MTK_CTRL_PLANE_H__
+
+#include <linux/kref.h>
+#include <linux/skbuff.h>
+
+#include "mtk_dev.h"
+
+struct mtk_ctrl_blk {
+ struct mtk_md_dev *mdev;
+ struct mtk_ctrl_trans *trans;
+};
+
+int mtk_ctrl_init(struct mtk_md_dev *mdev);
+void mtk_ctrl_exit(struct mtk_md_dev *mdev);
+
+#endif /* __MTK_CTRL_PLANE_H__ */
diff --git a/drivers/net/wwan/t9xx/mtk_dev.c b/drivers/net/wwan/t9xx/mtk_dev.c
new file mode 100644
index 000000000000..50f77d5a210c
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_dev.c
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/module.h>
+
+#include "mtk_dev.h"
+
+MODULE_DESCRIPTION("MediaTek T9xx PCIe WWAN driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/wwan/t9xx/mtk_dev.h b/drivers/net/wwan/t9xx/mtk_dev.h
index 8278a0e2875e..9843171fe2ae 100644
--- a/drivers/net/wwan/t9xx/mtk_dev.h
+++ b/drivers/net/wwan/t9xx/mtk_dev.h
@@ -36,6 +36,7 @@ enum mtk_dev_evt_d2h {
};
struct mtk_md_dev;
+struct mtk_ctrl_blk;
struct mtk_dev_ops {
u32 (*get_dev_state)(struct mtk_md_dev *mdev);
@@ -57,6 +58,7 @@ struct mtk_md_dev {
void *hw_priv;
u32 hw_ver;
char dev_str[MTK_DEV_STR_LEN];
+ struct mtk_ctrl_blk *ctrl_blk;
};
static inline u32 mtk_dev_get_dev_state(struct mtk_md_dev *mdev)
diff --git a/drivers/net/wwan/t9xx/pcie/Makefile b/drivers/net/wwan/t9xx/pcie/Makefile
new file mode 100644
index 000000000000..7410d1796d27
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+ccflags-y += -I$(src)
+ccflags-y += -I$(src)/..
+
+obj-$(CONFIG_MTK_T9XX_PCI) += mtk_t9xx_pcie.o
+
+mtk_t9xx_pcie-y := \
+ mtk_pci_drv_m9xx.o \
+ mtk_pci.o
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
index 1f6cca70b74c..9588635fab3d 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
@@ -14,6 +14,7 @@
#include <linux/module.h>
#include "mtk_dev.h"
+#include "mtk_trans_ctrl.h"
#include "mtk_pci.h"
#include "mtk_pci_reg.h"
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
new file mode 100644
index 000000000000..d6de4c43b529
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_TRANS_CTRL_H__
+#define __MTK_TRANS_CTRL_H__
+
+#include <linux/kref.h>
+#include <linux/list.h>
+#include <linux/skbuff.h>
+#include <linux/types.h>
+
+#include "mtk_dev.h"
+
+struct mtk_ctrl_trans {
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_md_dev *mdev;
+};
+
+#endif
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 4/7] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
` (4 subsequent siblings)
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
Cross Layer Direct Memory Access(CLDMA) is the hardware
interface used by the control plane and designated to
translate data between the host and the device. It supports
8 hardware queues for the device AP and modem respectively.
CLDMA driver uses General Purpose Descriptor (GPD) to
describe transaction information that can be recognized by
CLDMA hardware. Once CLDMA hardware transaction is started,
it would fetch and parse GPD to transfer data correctly.
To facilitate the CLDMA transaction, a GPD ring for each
queue is used. Once the transaction is started, CLDMA
hardware will traverse the GPD ring to transfer data between
the host and the device until no GPD is available.
CLDMA TX flow:
Once a TX service receives the TX data from the port layer,
it uses APIs exported by the CLDMA driver to configure GPD
with the DMA address of TX data. After that, the service
triggers CLDMA to fetch the first available GPD to transfer
data.
CLDMA RX flow:
When there is RX data from the MD, CLDMA hardware asserts an
interrupt to notify the host to fetch data and dispatch it
to FSM (for handshake messages) or the port layer.
After CLDMA opening is finished, All RX GPDs are fulfilled
and ready to receive data from the device.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/t9xx/mtk_ctrl_plane.c | 4 +-
drivers/net/wwan/t9xx/mtk_ctrl_plane.h | 52 +-
drivers/net/wwan/t9xx/pcie/Makefile | 7 +-
drivers/net/wwan/t9xx/pcie/mtk_cldma.c | 1232 +++++++++++++++++++++++
drivers/net/wwan/t9xx/pcie/mtk_cldma.h | 173 ++++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c | 369 +++++++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h | 177 ++++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c | 183 ++++
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h | 103 ++
drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c | 24 +
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 39 +
drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h | 1 +
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c | 541 ++++++++++
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h | 86 ++
14 files changed, 2987 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
index cf6079218e1c..4be699d1ec00 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
@@ -11,13 +11,14 @@
/**
* mtk_ctrl_init() - Initialize the control plane block.
* @mdev: Pointer to the MTK modem device.
+ * @ops: HIF operations for the control plane.
*
* Allocates and initializes the control plane block
* associated with @mdev.
*
* Return: 0 on success, -ENOMEM on allocation failure.
*/
-int mtk_ctrl_init(struct mtk_md_dev *mdev)
+int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops)
{
struct mtk_ctrl_blk *ctrl_blk;
@@ -27,6 +28,7 @@ int mtk_ctrl_init(struct mtk_md_dev *mdev)
ctrl_blk->mdev = mdev;
mdev->ctrl_blk = ctrl_blk;
+ ctrl_blk->ops = ops;
return 0;
}
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
index c141876ef95d..88d71ac92084 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
@@ -11,12 +11,60 @@
#include "mtk_dev.h"
+enum mtk_trb_cmd_type {
+ TRB_CMD_MIN,
+ TRB_CMD_ENABLE,
+ TRB_CMD_TX,
+ TRB_CMD_DISABLE,
+ TRB_CMD_STOP,
+ TRB_CMD_RECOVER,
+ TRB_CMD_MAX,
+};
+
+enum mtk_hif_dev_ctrl_cmd {
+ HIF_CTRL_CMD_CHECK_TX_FULL,
+};
+
+struct trb_open_priv {
+ u8 log_rg_offset;
+ u32 tx_mtu;
+ u32 rx_mtu;
+ u32 tx_frag_size;
+ u32 rx_frag_size;
+ int (*rx_done)(struct sk_buff *skb, void *priv, bool force_recv);
+};
+
+struct trb {
+ u32 channel_id;
+ enum mtk_trb_cmd_type cmd;
+ int status;
+ struct kref kref;
+ void *priv;
+ int (*trb_complete)(struct sk_buff *skb);
+};
+
+union ctrl_hif_cmd_data {
+ u32 rx_ch;
+};
+
+struct mtk_ctrl_hif_ops {
+ int (*init)(struct mtk_md_dev *mdev);
+ int (*exit)(struct mtk_md_dev *mdev);
+ int (*submit_skb)(struct mtk_md_dev *mdev, struct sk_buff *skb, bool force_send);
+ int (*send_cmd)(struct mtk_md_dev *mdev, int cmd, void *data);
+};
+
+struct mtk_ctrl_cfg;
+struct mtk_ctrl_trans;
+
struct mtk_ctrl_blk {
struct mtk_md_dev *mdev;
- struct mtk_ctrl_trans *trans;
+ struct mtk_ctrl_hif_ops *ops;
+ void *ctrl_hw_priv;
+ struct mtk_ctrl_cfg *cfg;
};
-int mtk_ctrl_init(struct mtk_md_dev *mdev);
+int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops);
void mtk_ctrl_exit(struct mtk_md_dev *mdev);
#endif /* __MTK_CTRL_PLANE_H__ */
diff --git a/drivers/net/wwan/t9xx/pcie/Makefile b/drivers/net/wwan/t9xx/pcie/Makefile
index 7410d1796d27..5252f158b058 100644
--- a/drivers/net/wwan/t9xx/pcie/Makefile
+++ b/drivers/net/wwan/t9xx/pcie/Makefile
@@ -7,4 +7,9 @@ obj-$(CONFIG_MTK_T9XX_PCI) += mtk_t9xx_pcie.o
mtk_t9xx_pcie-y := \
mtk_pci_drv_m9xx.o \
- mtk_pci.o
+ mtk_cldma_drv_m9xx.o \
+ mtk_ctrl_cfg_m9xx.o \
+ mtk_pci.o \
+ mtk_trans_ctrl.o \
+ mtk_cldma.o \
+ mtk_cldma_drv.o
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
new file mode 100644
index 000000000000..097b20d8fbd3
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
@@ -0,0 +1,1232 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kdev_t.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/netdevice.h>
+#include <linux/sched.h>
+#include <linux/skbuff.h>
+#include <linux/slab.h>
+#include <linux/timer.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include "mtk_pci.h"
+#include "mtk_cldma.h"
+#include "mtk_cldma_drv.h"
+#include "mtk_dev.h"
+
+#define cldma_drv_ops_null NULL
+#define DMA_POOL_NAME_LEN (64)
+#define WAIT_HWO_ROUND (10)
+#define WAIT_HWO_TIME (5)
+#define CLDMA_RETRY_DELAY_MS (100)
+#define NO_BUDGET (0)
+
+static const int mtk_cldma_hw_id_tbl[NR_CLDMA] = {
+ [CLDMA0] = CLDMA0_HW_ID,
+ [CLDMA1] = CLDMA1_HW_ID,
+ [CLDMA4] = CLDMA4_HW_ID,
+};
+
+static void mtk_cldma_clr_bd_dsc(struct cldma_drv_info *drv_info,
+ struct bd_dsc *bd_dsc_pool, int nr_bds)
+{
+ struct bd_dsc *bd_dsc;
+ int i;
+
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = bd_dsc_pool + i;
+ dma_unmap_single(drv_info->mdev->dev, bd_dsc->data_dma_addr,
+ bd_dsc->data_len, DMA_TO_DEVICE);
+ bd_dsc->data_dma_addr = 0;
+ bd_dsc->data_len = 0;
+ if (bd_dsc->bd->tx_bd.bd_flags & CLDMA_BD_FLAG_EOL) {
+ bd_dsc->bd->tx_bd.bd_flags &= ~CLDMA_BD_FLAG_EOL;
+ break;
+ }
+ }
+}
+
+static void mtk_cldma_tx_done_work(struct work_struct *work)
+{
+ struct txq *txq = container_of(work, struct txq, tx_done_work);
+ struct cldma_drv_info *drv_info;
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_ctrl_trans *trans;
+ struct mtk_md_dev *mdev;
+ struct tx_req *req;
+ unsigned int state;
+ struct trb *trb;
+ int i, hif_id;
+ u32 txqno;
+
+ drv_info = txq->drv_info;
+ hif_id = drv_info->hif_id;
+ txqno = txq->txqno;
+ mdev = drv_info->mdev;
+ drv_ops = drv_info->drv_ops;
+ trans = drv_info->cd->trans;
+
+again:
+ for (i = 0; i < txq->nr_gpds; i++) {
+ if (READ_ONCE(txq->free_idx) == READ_ONCE(txq->wr_idx))
+ break;
+
+ req = txq->req_pool + txq->free_idx;
+
+ if (!req->data_vm_addr || (req->gpd->tx_gpd.gpd_flags & CLDMA_GPD_FLAG_HWO))
+ break;
+
+ dma_rmb(); /* read descriptor fields after HWO check */
+
+ if (txq->nr_bds)
+ mtk_cldma_clr_bd_dsc(drv_info, req->bd_dsc_pool, txq->nr_bds);
+ else
+ dma_unmap_single(mdev->dev, req->data_dma_addr,
+ req->data_len, DMA_TO_DEVICE);
+
+ trb = (struct trb *)req->skb->cb;
+ trb->status = 0;
+ trb->trb_complete(req->skb);
+
+ req->data_vm_addr = NULL;
+ req->data_dma_addr = 0;
+ req->data_len = 0;
+ req->skb = NULL;
+
+ txq->free_idx = (txq->free_idx + 1) % txq->nr_gpds;
+ if (atomic_fetch_inc(&txq->req_budget) == NO_BUDGET)
+ wake_up(&trans->trb_srv[trans->srv_cfg[hif_id][txqno]]->trb_waitq);
+ }
+
+ state = drv_ops->cldma_check_intr_status(drv_info, DIR_TX, txqno, QUEUE_XFER_DONE);
+ if (state) {
+ if (unlikely(state == LINK_ERROR_VAL))
+ goto out;
+
+ drv_ops->cldma_clr_intr_status(drv_info, DIR_TX, txqno, QUEUE_XFER_DONE);
+
+ cond_resched();
+
+ goto again;
+ }
+
+out:
+ drv_ops->cldma_unmask_intr(drv_info, DIR_TX, txqno, QUEUE_XFER_DONE);
+}
+
+static void mtk_cldma_rx_skb_adjust(struct mtk_md_dev *mdev, struct rxq *rxq,
+ struct rx_req *req)
+{
+ struct bd_dsc *bd_dsc;
+ int i;
+
+ for (i = 0; i < rxq->nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ if (bd_dsc->data_dma_addr) {
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ req->frag_size, DMA_FROM_DEVICE);
+ bd_dsc->data_dma_addr = 0;
+ }
+ bd_dsc->skb->len = 0;
+ skb_reset_tail_pointer(bd_dsc->skb);
+ skb_put(bd_dsc->skb,
+ min_t(u16, le16_to_cpu(bd_dsc->bd->rx_bd.data_recv_len),
+ req->frag_size));
+ if (req->skb != bd_dsc->skb) {
+ req->skb->len += bd_dsc->skb->len;
+ req->skb->data_len += bd_dsc->skb->len;
+ }
+ bd_dsc->bd->rx_bd.data_recv_len = 0;
+ bd_dsc->skb = NULL;
+ }
+ if (!rxq->nr_bds) {
+ if (req->data_dma_addr) {
+ dma_unmap_single(mdev->dev, req->data_dma_addr,
+ req->mtu, DMA_FROM_DEVICE);
+ req->data_dma_addr = 0;
+ }
+ req->skb->len = 0;
+ skb_reset_tail_pointer(req->skb);
+ skb_put(req->skb,
+ min_t(u16, le16_to_cpu(req->gpd->rx_gpd.data_recv_len),
+ req->mtu));
+ }
+
+ req->gpd->rx_gpd.data_recv_len = 0;
+}
+
+static int mtk_cldma_reload_rx_skb(struct mtk_md_dev *mdev, struct rxq *rxq,
+ struct rx_req *req)
+{
+ struct sk_buff *tail = NULL;
+ struct bd_dsc *bd_dsc;
+ int nr_bds;
+ int i, ret;
+
+ nr_bds = rxq->nr_bds;
+
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ bd_dsc->skb = __dev_alloc_skb(req->frag_size, GFP_KERNEL);
+ if (!bd_dsc->skb) {
+ dev_warn_ratelimited(mdev->dev, "Failed to alloc SKB\n");
+ ret = -ENOMEM;
+ goto err_free_skb;
+ }
+ bd_dsc->skb->next = NULL;
+ bd_dsc->data_dma_addr = dma_map_single(mdev->dev, bd_dsc->skb->data,
+ req->frag_size, DMA_FROM_DEVICE);
+ ret = dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr);
+ if (unlikely(ret)) {
+ dev_warn_ratelimited(mdev->dev, "Failed to map SKB data\n");
+ ret = -EFAULT;
+ goto err_free_skb;
+ }
+ bd_dsc->bd->rx_bd.data_buff_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->data_dma_addr) >> 32);
+ bd_dsc->bd->rx_bd.data_buff_ptr_l =
+ cpu_to_le32(bd_dsc->data_dma_addr);
+ if (tail) {
+ tail->next = bd_dsc->skb;
+ tail = bd_dsc->skb;
+ continue;
+ }
+ if (!req->skb) {
+ req->skb = bd_dsc->skb;
+ } else {
+ skb_shinfo(req->skb)->frag_list = bd_dsc->skb;
+ tail = bd_dsc->skb;
+ }
+ }
+ if (!nr_bds) {
+ req->skb = __dev_alloc_skb(req->mtu, GFP_KERNEL);
+ if (!req->skb) {
+ ret = -ENOMEM;
+ goto err_free_skb;
+ }
+
+ req->data_dma_addr = dma_map_single(mdev->dev, req->skb->data,
+ req->mtu, DMA_FROM_DEVICE);
+ ret = dma_mapping_error(mdev->dev, req->data_dma_addr);
+ if (unlikely(ret)) {
+ dev_warn_ratelimited(mdev->dev, "Failed to map SKB data\n");
+ ret = -EFAULT;
+ goto err_free_skb;
+ }
+ req->gpd->rx_gpd.data_buff_ptr_h = cpu_to_le32((u64)req->data_dma_addr >> 32);
+ req->gpd->rx_gpd.data_buff_ptr_l = cpu_to_le32(req->data_dma_addr);
+ }
+ return 0;
+
+err_free_skb:
+ if (nr_bds) {
+ if (req->skb)
+ skb_shinfo(req->skb)->frag_list = NULL;
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ if (!bd_dsc->skb)
+ break;
+ if (!dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr))
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ req->frag_size, DMA_FROM_DEVICE);
+ bd_dsc->data_dma_addr = 0;
+ bd_dsc->skb->next = NULL;
+ dev_kfree_skb_any(bd_dsc->skb);
+ bd_dsc->skb = NULL;
+ }
+ } else {
+ req->data_dma_addr = 0;
+ if (req->skb)
+ dev_kfree_skb_any(req->skb);
+ }
+ req->skb = NULL;
+
+ return ret;
+}
+
+static int mtk_cldma_check_rx_req(struct cldma_drv_info *drv_info, struct rxq *rxq)
+{
+ struct rx_req *req = rxq->req_pool + rxq->free_idx;
+ u64 curr_addr;
+ int i;
+
+ curr_addr = drv_info->drv_ops->cldma_get_rx_curr_addr(drv_info, rxq->rxqno);
+ if (unlikely(!curr_addr))
+ return -ENXIO;
+
+ if (req->gpd_dma_addr == curr_addr)
+ return -EAGAIN;
+ for (i = 0; i < WAIT_HWO_ROUND; i++) {
+ udelay(WAIT_HWO_TIME);
+ if (!(READ_ONCE(req->gpd->rx_gpd.gpd_flags) & CLDMA_GPD_FLAG_HWO))
+ break;
+ }
+ if (i == WAIT_HWO_ROUND) {
+ dev_err((drv_info->mdev)->dev, "Failed to check HWO=0\n");
+ return -EAGAIN;
+ }
+
+ return 0;
+}
+
+static bool mtk_cldma_rx_check_again(struct rxq *rxq)
+{
+ struct cldma_drv_info *drv_info;
+ struct cldma_drv_ops *drv_ops;
+ bool need_check_again = false;
+ u32 state;
+ int rxqno;
+
+ drv_info = rxq->drv_info;
+ drv_ops = drv_info->drv_ops;
+ rxqno = rxq->rxqno;
+
+ do {
+ state = drv_ops->cldma_check_intr_status(drv_info, DIR_RX,
+ rxqno, QUEUE_XFER_DONE);
+ if (state) {
+ if (unlikely(state == LINK_ERROR_VAL))
+ break;
+
+ drv_ops->cldma_clr_intr_status(drv_info, DIR_RX,
+ rxqno, QUEUE_XFER_DONE);
+ cond_resched();
+ return true;
+ }
+ } while (need_check_again);
+
+ return false;
+}
+
+static void mtk_cldma_rx_done_work(struct work_struct *work)
+{
+ struct rx_req *req = NULL, *pre_req = NULL;
+ struct rxq *rxq = container_of(work, struct rxq, rx_done_work);
+ struct cldma_drv_info *drv_info;
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_md_dev *mdev;
+ struct sk_buff *rx_skb;
+ int i, ret, idx;
+
+ drv_info = rxq->drv_info;
+ mdev = drv_info->mdev;
+ drv_ops = drv_info->drv_ops;
+
+again:
+ for (i = 0; i < rxq->nr_gpds; i++) {
+ req = rxq->req_pool + rxq->free_idx;
+ if (!req->skb) {
+ dev_err(mdev->dev,
+ "Failed to get valid req cldma%d rxq%d req%d\n",
+ drv_info->hw_id, rxq->rxqno, rxq->free_idx);
+ goto out;
+ }
+
+ if (req->gpd->rx_gpd.gpd_flags & CLDMA_GPD_FLAG_HWO)
+ break;
+
+ dma_rmb(); /* read descriptor fields after HWO check */
+
+ mtk_cldma_rx_skb_adjust(mdev, rxq, req);
+ rx_skb = req->skb;
+ req->skb = NULL;
+
+ ret = mtk_cldma_reload_rx_skb(mdev, rxq, req);
+ if (ret) {
+ /* Alloc failed — recycle old buffer, drop packet.
+ * BD mode cannot recycle directly (BDP flag mismatch),
+ * so accept the stall and let reset recovery handle it.
+ */
+ if (rxq->nr_bds) {
+ dev_kfree_skb_any(rx_skb);
+ goto out;
+ }
+
+ skb_trim(rx_skb, 0);
+ req->skb = rx_skb;
+ req->data_dma_addr = dma_map_single(mdev->dev,
+ rx_skb->data,
+ req->mtu,
+ DMA_FROM_DEVICE);
+ if (dma_mapping_error(mdev->dev, req->data_dma_addr)) {
+ req->data_dma_addr = 0;
+ dev_kfree_skb_any(rx_skb);
+ req->skb = NULL;
+ /* Advance free_idx below to prevent ring stall */
+ } else {
+ req->gpd->rx_gpd.data_buff_ptr_h =
+ cpu_to_le32((u64)req->data_dma_addr >> 32);
+ req->gpd->rx_gpd.data_buff_ptr_l =
+ cpu_to_le32(req->data_dma_addr);
+ }
+ } else {
+ do {
+ ret = rxq->rx_done(rx_skb, rxq->arg,
+ atomic_read(&rxq->need_exit) ? true : false);
+ if (ret == -EAGAIN)
+ usleep_range(1000, 2000);
+ } while (ret == -EAGAIN);
+ }
+
+ wmb(); /* ensure addr set done before HWO setup done */
+
+ idx = rxq->free_idx == 0 ? rxq->nr_gpds - 1 : rxq->free_idx - 1;
+ pre_req = rxq->req_pool + idx;
+ pre_req->gpd->rx_gpd.gpd_flags |= CLDMA_GPD_FLAG_HWO;
+ rxq->free_idx = (rxq->free_idx + 1) % rxq->nr_gpds;
+ }
+
+ ret = mtk_cldma_check_rx_req(drv_info, rxq);
+ if (!ret)
+ goto again;
+ else if (ret == -ENXIO)
+ goto out;
+
+ if (!atomic_read(&rxq->need_exit))
+ drv_ops->cldma_resume_queue(drv_info, DIR_RX, rxq->rxqno);
+
+ if (mtk_cldma_rx_check_again(rxq))
+ goto again;
+
+out:
+ drv_ops->cldma_unmask_intr(drv_info, DIR_RX, rxq->rxqno, QUEUE_XFER_DONE);
+ drv_ops->cldma_clear_ip_busy(drv_info);
+}
+
+static int mtk_cldma_alloc_tx_bd(struct cldma_drv_info *drv_info, struct txq *txq,
+ struct tx_req *req)
+{
+ struct bd_dsc *bd_dsc, *last_bd_dsc = NULL;
+ int i;
+
+ req->bd_dsc_pool = kcalloc(txq->nr_bds, sizeof(*bd_dsc),
+ GFP_KERNEL);
+ if (!req->bd_dsc_pool)
+ return -ENOMEM;
+
+ for (i = 0; i < txq->nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ bd_dsc->bd = dma_pool_zalloc(drv_info->bd_dma_pool, GFP_KERNEL,
+ &bd_dsc->bd_dma_addr);
+ if (!bd_dsc->bd)
+ return -ENOMEM;
+ if (!last_bd_dsc) {
+ req->gpd->tx_gpd.data_buff_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->bd_dma_addr) >> 32);
+ req->gpd->tx_gpd.data_buff_ptr_l =
+ cpu_to_le32(bd_dsc->bd_dma_addr);
+ } else {
+ last_bd_dsc->bd->tx_bd.next_bd_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->bd_dma_addr) >> 32);
+ last_bd_dsc->bd->tx_bd.next_bd_ptr_l =
+ cpu_to_le32(bd_dsc->bd_dma_addr);
+ }
+ last_bd_dsc = bd_dsc;
+ }
+ return 0;
+}
+
+static struct txq *mtk_cldma_txq_alloc(struct cldma_drv_info *drv_info, struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_ctrl_trans *trans;
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_md_dev *mdev;
+ struct bd_dsc *bd_dsc;
+ struct tx_req *next;
+ struct tx_req *req;
+ u16 tx_frag_size;
+ struct txq *txq;
+ int i, j, ret;
+
+ mdev = drv_info->mdev;
+ ctrl_blk = mdev->ctrl_blk;
+ trans = ctrl_blk->ctrl_hw_priv;
+ drv_ops = drv_info->drv_ops;
+
+ txq = kzalloc_obj(*txq);
+ if (!txq)
+ return NULL;
+
+ txq->que = radix_tree_lookup(&trans->queue_tbl, trb->channel_id & 0xFFFF);
+ txq->drv_info = drv_info;
+ txq->txqno = txq->que->txqno;
+ txq->nr_gpds = txq->que->tx_nr_gpds;
+ atomic_set(&txq->req_budget, txq->que->tx_nr_gpds);
+ txq->is_stopping = false;
+ tx_frag_size = txq->que->tx_frag_size;
+ if (txq->que->tx_mtu > tx_frag_size && tx_frag_size)
+ txq->nr_bds = (txq->que->tx_mtu + tx_frag_size - 1) / tx_frag_size;
+
+ txq->req_pool = kcalloc(txq->nr_gpds, sizeof(*req), GFP_KERNEL);
+ if (!txq->req_pool)
+ goto err_free_txq;
+
+ for (i = 0; i < txq->nr_gpds; i++) {
+ req = txq->req_pool + i;
+ req->mtu = txq->que->tx_mtu;
+ req->frag_size = tx_frag_size;
+ req->gpd = dma_pool_zalloc(drv_info->gpd_dma_pool, GFP_KERNEL, &req->gpd_dma_addr);
+ if (!req->gpd)
+ goto err_free_req;
+ if (txq->nr_bds) {
+ ret = mtk_cldma_alloc_tx_bd(drv_info, txq, req);
+ if (ret)
+ goto err_free_req;
+ req->gpd->tx_gpd.gpd_flags |= CLDMA_GPD_FLAG_BDP;
+ }
+ }
+
+ for (i = 0; i < txq->nr_gpds; i++) {
+ req = txq->req_pool + i;
+ next = txq->req_pool + ((i + 1) % txq->nr_gpds);
+ req->gpd->tx_gpd.gpd_flags |= CLDMA_GPD_FLAG_IOC;
+ req->gpd->tx_gpd.next_gpd_ptr_h = cpu_to_le32((u64)(next->gpd_dma_addr) >> 32);
+ req->gpd->tx_gpd.next_gpd_ptr_l = cpu_to_le32(next->gpd_dma_addr);
+ }
+
+ INIT_WORK(&txq->tx_done_work, mtk_cldma_tx_done_work);
+
+ drv_ops->cldma_stop_queue(drv_info, DIR_TX, txq->txqno);
+ txq->tx_started = false;
+ drv_ops->cldma_setup_start_addr(drv_info, DIR_TX, txq->txqno,
+ txq->req_pool[0].gpd_dma_addr);
+ drv_ops->cldma_unmask_intr(drv_info, DIR_TX, txq->txqno, QUEUE_ERROR);
+ drv_ops->cldma_unmask_intr(drv_info, DIR_TX, txq->txqno, QUEUE_XFER_DONE);
+
+ drv_info->txq[txq->txqno] = txq;
+ return txq;
+
+err_free_req:
+ for (i = 0; i < txq->nr_gpds; i++) {
+ req = txq->req_pool + i;
+ if (!req->gpd)
+ break;
+ if (req->bd_dsc_pool) {
+ for (j = 0; j < txq->nr_bds; j++) {
+ bd_dsc = req->bd_dsc_pool + j;
+ if (!bd_dsc->bd)
+ break;
+ dma_pool_free(drv_info->bd_dma_pool, bd_dsc->bd,
+ bd_dsc->bd_dma_addr);
+ }
+ kfree(req->bd_dsc_pool);
+ }
+ dma_pool_free(drv_info->gpd_dma_pool, req->gpd, req->gpd_dma_addr);
+ }
+ kfree(txq->req_pool);
+err_free_txq:
+ kfree(txq);
+ return NULL;
+}
+
+static void mtk_cldma_txq_free(struct cldma_drv_info *drv_info, u32 txqno)
+{
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_md_dev *mdev;
+ struct bd_dsc *bd_dsc;
+ struct tx_req *req;
+ struct txq *txq;
+ struct trb *trb;
+ int irq_id;
+ int i, j;
+
+ mdev = drv_info->mdev;
+ drv_ops = drv_info->drv_ops;
+
+ txq = drv_info->txq[txqno];
+ drv_info->txq[txqno] = NULL;
+ /* stop HW tx transaction */
+ drv_ops->cldma_stop_queue(drv_info, DIR_TX, txqno);
+ txq->tx_started = false;
+
+ irq_id = mtk_pci_get_virq_id(mdev, drv_info->pci_ext_irq_id);
+ synchronize_irq(irq_id);
+ /* flush on-going work */
+ flush_work(&txq->tx_done_work);
+ drv_ops->cldma_mask_intr(drv_info, DIR_TX, txqno, QUEUE_XFER_DONE);
+ drv_ops->cldma_mask_intr(drv_info, DIR_TX, txqno, QUEUE_ERROR);
+
+ /* free tx req resource */
+ for (i = 0; i < txq->nr_gpds; i++) {
+ req = txq->req_pool + txq->free_idx;
+ if (req->skb && req->data_len) {
+ if (!txq->nr_bds)
+ dma_unmap_single(mdev->dev, req->data_dma_addr,
+ req->data_len, DMA_TO_DEVICE);
+ for (j = 0; j < txq->nr_bds; j++) {
+ bd_dsc = req->bd_dsc_pool + j;
+ if (!bd_dsc->data_dma_addr)
+ continue;
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ bd_dsc->data_len, DMA_TO_DEVICE);
+ }
+ trb = (struct trb *)req->skb->cb;
+ trb->status = -EPIPE;
+ trb->trb_complete(req->skb);
+ }
+ for (j = 0; j < txq->nr_bds; j++) {
+ bd_dsc = req->bd_dsc_pool + j;
+ dma_pool_free(drv_info->bd_dma_pool, bd_dsc->bd,
+ bd_dsc->bd_dma_addr);
+ }
+ kfree(req->bd_dsc_pool);
+ dma_pool_free(drv_info->gpd_dma_pool, req->gpd, req->gpd_dma_addr);
+ txq->free_idx = (txq->free_idx + 1) % txq->nr_gpds;
+ }
+
+ kfree(txq->req_pool);
+ kfree(txq);
+}
+
+static int mtk_cldma_alloc_rx_bd(struct cldma_drv_info *drv_info, struct rx_req *req,
+ int nr_bds)
+{
+ struct bd_dsc *bd_dsc, *last_bd_dsc = NULL;
+ struct sk_buff *tail = NULL;
+ struct mtk_md_dev *mdev;
+ u32 left_size;
+ int ret;
+ int i;
+
+ mdev = drv_info->mdev;
+ left_size = req->mtu;
+
+ req->bd_dsc_pool = kcalloc(nr_bds, sizeof(*bd_dsc),
+ GFP_KERNEL);
+ if (!req->bd_dsc_pool)
+ return -ENOMEM;
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ bd_dsc->bd = dma_pool_zalloc(drv_info->bd_dma_pool, GFP_KERNEL,
+ &bd_dsc->bd_dma_addr);
+ if (!bd_dsc->bd)
+ return -ENOMEM;
+
+ bd_dsc->skb = __dev_alloc_skb(req->frag_size, GFP_KERNEL);
+ if (!bd_dsc->skb)
+ return -ENOMEM;
+ bd_dsc->skb->next = NULL;
+ bd_dsc->data_dma_addr =
+ dma_map_single(mdev->dev, bd_dsc->skb->data,
+ req->frag_size, DMA_FROM_DEVICE);
+ ret = dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr);
+ if (unlikely(ret))
+ return -ENOMEM;
+
+ bd_dsc->bd->rx_bd.data_buff_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->data_dma_addr) >> 32);
+ bd_dsc->bd->rx_bd.data_buff_ptr_l =
+ cpu_to_le32(bd_dsc->data_dma_addr);
+ bd_dsc->bd->rx_bd.data_allow_len =
+ cpu_to_le16(min(req->frag_size, left_size));
+ left_size -= min(req->frag_size, left_size);
+ if (!last_bd_dsc) {
+ req->gpd->rx_gpd.data_buff_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->bd_dma_addr) >> 32);
+ req->gpd->rx_gpd.data_buff_ptr_l =
+ cpu_to_le32(bd_dsc->bd_dma_addr);
+ } else {
+ last_bd_dsc->bd->rx_bd.next_bd_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->bd_dma_addr) >> 32);
+ last_bd_dsc->bd->rx_bd.next_bd_ptr_l =
+ cpu_to_le32(bd_dsc->bd_dma_addr);
+ }
+ last_bd_dsc = bd_dsc;
+ if (tail) {
+ tail->next = bd_dsc->skb;
+ tail = bd_dsc->skb;
+ continue;
+ }
+ if (!req->skb) {
+ req->skb = bd_dsc->skb;
+ } else {
+ skb_shinfo(req->skb)->frag_list = bd_dsc->skb;
+ tail = bd_dsc->skb;
+ }
+ }
+ last_bd_dsc->bd->rx_bd.bd_flags |= CLDMA_BD_FLAG_EOL;
+ return 0;
+}
+
+static void mtk_cldma_rxq_alloc_cancel(struct cldma_drv_info *drv_info, struct rx_req *req,
+ int nr_bds)
+{
+ struct mtk_md_dev *mdev;
+ struct bd_dsc *bd_dsc;
+ int i;
+
+ mdev = drv_info->mdev;
+
+ if (nr_bds) {
+ if (req->skb)
+ skb_shinfo(req->skb)->frag_list = NULL;
+ if (req->bd_dsc_pool) {
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ if (!bd_dsc->bd)
+ break;
+ if (bd_dsc->skb) {
+ if (!dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr))
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ req->frag_size, DMA_FROM_DEVICE);
+ bd_dsc->data_dma_addr = 0;
+ bd_dsc->skb->next = NULL;
+ dev_kfree_skb_any(bd_dsc->skb);
+ }
+ dma_pool_free(drv_info->bd_dma_pool, bd_dsc->bd,
+ bd_dsc->bd_dma_addr);
+ }
+ kfree(req->bd_dsc_pool);
+ }
+ } else {
+ if (req->skb) {
+ if (!dma_mapping_error(mdev->dev, req->data_dma_addr))
+ dma_unmap_single(mdev->dev, req->data_dma_addr,
+ req->mtu, DMA_FROM_DEVICE);
+ req->data_dma_addr = 0;
+ dev_kfree_skb_any(req->skb);
+ }
+ }
+ dma_pool_free(drv_info->gpd_dma_pool, req->gpd, req->gpd_dma_addr);
+}
+
+static struct rxq *mtk_cldma_rxq_alloc(struct cldma_drv_info *drv_info, struct sk_buff *skb)
+{
+ struct trb_open_priv *trb_open_priv = (struct trb_open_priv *)skb->data;
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_ctrl_trans *trans;
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_md_dev *mdev;
+ struct rx_req *next;
+ struct rx_req *req;
+ u16 rx_frag_size;
+ struct rxq *rxq;
+ int ret;
+ int i;
+
+ mdev = drv_info->mdev;
+ ctrl_blk = mdev->ctrl_blk;
+ trans = ctrl_blk->ctrl_hw_priv;
+ drv_ops = drv_info->drv_ops;
+
+ rxq = kzalloc_obj(*rxq);
+ if (!rxq)
+ return NULL;
+
+ rxq->que = radix_tree_lookup(&trans->queue_tbl, trb->channel_id & 0xFFFF);
+ if (rxq->que->rx_nr_gpds < MIN_GPD_NUM) {
+ dev_err(mdev->dev,
+ "Failed to alloc cldma%d rxq%d due to gpd number < 2\n",
+ drv_info->hw_id, rxq->rxqno);
+ goto err_free_rxq;
+ }
+ rxq->drv_info = drv_info;
+ rxq->rxqno = rxq->que->rxqno;
+ rxq->nr_gpds = rxq->que->rx_nr_gpds;
+ rxq->arg = trb->priv;
+ rxq->rx_done = trb_open_priv->rx_done;
+ atomic_set(&rxq->need_exit, 0);
+ rx_frag_size = rxq->que->rx_frag_size;
+ if (rxq->que->rx_mtu > rx_frag_size && rx_frag_size)
+ rxq->nr_bds = (rxq->que->rx_mtu + rx_frag_size - 1) / rx_frag_size;
+
+ rxq->req_pool = kcalloc(rxq->nr_gpds, sizeof(*req), GFP_KERNEL);
+ if (!rxq->req_pool)
+ goto err_free_rxq;
+
+ /* setup rx request */
+ for (i = 0; i < rxq->nr_gpds; i++) {
+ req = rxq->req_pool + i;
+ req->mtu = rxq->que->rx_mtu;
+ req->frag_size = rx_frag_size;
+ req->gpd = dma_pool_zalloc(drv_info->gpd_dma_pool, GFP_KERNEL, &req->gpd_dma_addr);
+ if (!req->gpd)
+ goto err_free_req;
+ if (rxq->nr_bds) {
+ ret = mtk_cldma_alloc_rx_bd(drv_info, req, rxq->nr_bds);
+ if (ret)
+ goto err_free_req;
+ req->gpd->rx_gpd.gpd_flags |= CLDMA_GPD_FLAG_BDP;
+ } else {
+ req->skb = __dev_alloc_skb(req->mtu, GFP_KERNEL);
+ if (!req->skb)
+ goto err_free_req;
+ req->data_dma_addr = dma_map_single(mdev->dev, req->skb->data,
+ req->mtu, DMA_FROM_DEVICE);
+ ret = dma_mapping_error(mdev->dev, req->data_dma_addr);
+ if (unlikely(ret))
+ goto err_free_req;
+ }
+ }
+
+ for (i = 0; i < rxq->nr_gpds; i++) {
+ req = rxq->req_pool + i;
+ next = rxq->req_pool + ((i + 1) % rxq->nr_gpds);
+ req->gpd->rx_gpd.gpd_flags |= CLDMA_GPD_FLAG_IOC;
+ req->gpd->rx_gpd.data_allow_len = cpu_to_le16(req->mtu);
+ req->gpd->rx_gpd.next_gpd_ptr_h = cpu_to_le32((u64)(next->gpd_dma_addr) >> 32);
+ req->gpd->rx_gpd.next_gpd_ptr_l = cpu_to_le32(next->gpd_dma_addr);
+ if (!rxq->nr_bds) {
+ req->gpd->rx_gpd.data_buff_ptr_h =
+ cpu_to_le32((u64)(req->data_dma_addr) >> 32);
+ req->gpd->rx_gpd.data_buff_ptr_l = cpu_to_le32(req->data_dma_addr);
+ }
+ if (i != rxq->nr_gpds - 1)
+ req->gpd->rx_gpd.gpd_flags |= CLDMA_GPD_FLAG_HWO;
+ }
+
+ INIT_WORK(&rxq->rx_done_work, mtk_cldma_rx_done_work);
+
+ drv_info->rxq[rxq->rxqno] = rxq;
+ drv_ops->cldma_stop_queue(drv_info, DIR_RX, rxq->rxqno);
+ drv_ops->cldma_setup_start_addr(drv_info, DIR_RX,
+ rxq->rxqno, rxq->req_pool[0].gpd_dma_addr);
+ drv_ops->cldma_start_queue(drv_info, DIR_RX, rxq->rxqno);
+ drv_ops->cldma_unmask_intr(drv_info, DIR_RX, rxq->rxqno, QUEUE_ERROR);
+ drv_ops->cldma_unmask_intr(drv_info, DIR_RX, rxq->rxqno, QUEUE_XFER_DONE);
+
+ return rxq;
+
+err_free_req:
+ for (i = 0; i < rxq->nr_gpds; i++) {
+ req = rxq->req_pool + i;
+ if (!req->gpd)
+ break;
+ mtk_cldma_rxq_alloc_cancel(drv_info, req, rxq->nr_bds);
+ }
+
+ kfree(rxq->req_pool);
+err_free_rxq:
+ kfree(rxq);
+ return NULL;
+}
+
+static void mtk_cldma_rxq_free(struct cldma_drv_info *drv_info, u32 rxqno)
+{
+ struct cldma_drv_ops *drv_ops;
+ struct mtk_md_dev *mdev;
+ struct bd_dsc *bd_dsc;
+ struct rx_req *req;
+ struct rxq *rxq;
+ int irq_id;
+ int i, j;
+
+ mdev = drv_info->mdev;
+ drv_ops = drv_info->drv_ops;
+
+ rxq = drv_info->rxq[rxqno];
+ drv_info->rxq[rxqno] = NULL;
+
+ /* stop HW rx transaction */
+ atomic_set(&rxq->need_exit, 1);
+ drv_ops->cldma_stop_queue(drv_info, DIR_RX, rxqno);
+
+ irq_id = mtk_pci_get_virq_id(mdev, drv_info->pci_ext_irq_id);
+ synchronize_irq(irq_id);
+ /* flush on-going work */
+ flush_work(&rxq->rx_done_work);
+ /* mask L2 RX interrupt again to avoid race condition causing use-after-free issue */
+ drv_ops->cldma_mask_intr(drv_info, DIR_RX, rxqno, QUEUE_XFER_DONE);
+ drv_ops->cldma_mask_intr(drv_info, DIR_RX, rxqno, QUEUE_ERROR);
+
+ /* free rx req resource */
+ for (i = 0; i < rxq->nr_gpds; i++) {
+ req = rxq->req_pool + rxq->free_idx;
+ if (!(req->gpd->rx_gpd.gpd_flags & CLDMA_GPD_FLAG_HWO) &&
+ le16_to_cpu(req->gpd->rx_gpd.data_recv_len)) {
+ mtk_cldma_rx_skb_adjust(mdev, rxq, req);
+ rxq->rx_done(req->skb, rxq->arg, true);
+ req->skb = NULL;
+ }
+ if (req->skb) {
+ if (rxq->nr_bds) {
+ skb_shinfo(req->skb)->frag_list = NULL;
+ } else {
+ if (req->data_dma_addr)
+ dma_unmap_single(mdev->dev, req->data_dma_addr,
+ req->mtu, DMA_FROM_DEVICE);
+ dev_kfree_skb_any(req->skb);
+ }
+ }
+ for (j = 0; j < rxq->nr_bds; j++) {
+ bd_dsc = req->bd_dsc_pool + j;
+ if (bd_dsc->skb) {
+ if (bd_dsc->data_dma_addr)
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ req->frag_size, DMA_FROM_DEVICE);
+ bd_dsc->skb->next = NULL;
+ dev_kfree_skb_any(bd_dsc->skb);
+ }
+ dma_pool_free(drv_info->bd_dma_pool,
+ bd_dsc->bd, bd_dsc->bd_dma_addr);
+ }
+ kfree(req->bd_dsc_pool);
+ dma_pool_free(drv_info->gpd_dma_pool, req->gpd, req->gpd_dma_addr);
+ rxq->free_idx = (rxq->free_idx + 1) % rxq->nr_gpds;
+ }
+
+ kfree(rxq->req_pool);
+ kfree(rxq);
+}
+
+static int mtk_cldma_start_xfer(struct cldma_drv_info *drv_info, u32 qno)
+{
+ struct cldma_drv_ops *drv_ops;
+ struct txq *txq;
+ u32 val;
+
+ txq = drv_info->txq[qno];
+ drv_ops = drv_info->drv_ops;
+
+ val = drv_ops->cldma_get_tx_start_addr(drv_info, qno);
+ if (unlikely(val == LINK_ERROR_VAL))
+ return -EIO;
+
+ if (unlikely(!val)) {
+ drv_ops->cldma_drv_init(drv_info);
+ txq = drv_info->txq[qno];
+ drv_ops->cldma_setup_start_addr(drv_info, DIR_TX, qno,
+ txq->req_pool[txq->free_idx].gpd_dma_addr);
+ drv_ops->cldma_start_queue(drv_info, DIR_TX, qno);
+ txq->tx_started = true;
+ } else if (unlikely(!txq->tx_started)) {
+ drv_ops->cldma_start_queue(drv_info, DIR_TX, qno);
+ txq->tx_started = true;
+ } else {
+ drv_ops->cldma_resume_queue(drv_info, DIR_TX, qno);
+ }
+
+ return 0;
+}
+
+int mtk_cldma_init(struct mtk_ctrl_trans *trans)
+{
+ struct cldma_dev *cd;
+
+ cd = kzalloc_obj(*cd);
+ if (!cd)
+ return -ENOMEM;
+
+ cd->trans = trans;
+ trans->dev = cd;
+
+ return 0;
+}
+
+void mtk_cldma_exit(struct mtk_ctrl_trans *trans)
+{
+ if (!trans->dev)
+ return;
+
+ kfree(trans->dev);
+ trans->dev = NULL;
+}
+
+static int mtk_cldma_open(struct cldma_dev *cd, struct sk_buff *skb)
+{
+ struct trb_open_priv *trb_open_priv = (struct trb_open_priv *)skb->data;
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_info *drv_info;
+ struct queue_info *que;
+ struct txq *txq;
+ struct rxq *rxq;
+ int ret = 0;
+
+ que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
+ drv_info = cd->cldma_drv_info[que->hif_id];
+ if (!drv_info) {
+ ret = -EIO;
+ goto out;
+ }
+
+ if (que->tx_mtu == 0 || que->rx_mtu == 0) {
+ dev_err((cd->trans->mdev)->dev,
+ "Failed to enable cldma%d txq%d rxq%d due to wrong mtu\n",
+ drv_info->hw_id, que->txqno, que->rxqno);
+ ret = -EINVAL;
+ goto out;
+ }
+
+ trb_open_priv->tx_mtu = que->tx_mtu;
+ trb_open_priv->rx_mtu = que->rx_mtu;
+ trb_open_priv->tx_frag_size = que->tx_frag_size;
+ trb_open_priv->rx_frag_size = que->rx_frag_size;
+
+ if (drv_info->txq[que->txqno] || drv_info->rxq[que->rxqno]) {
+ ret = -EBUSY;
+ goto out;
+ }
+
+ txq = mtk_cldma_txq_alloc(drv_info, skb);
+ if (!txq) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ rxq = mtk_cldma_rxq_alloc(drv_info, skb);
+ if (!rxq) {
+ ret = -ENOMEM;
+ mtk_cldma_txq_free(drv_info, txq->txqno);
+ goto out;
+ }
+
+out:
+ trb->status = ret;
+ trb->trb_complete(skb);
+
+ return ret;
+}
+
+static int mtk_cldma_tx(struct cldma_dev *cd, struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_info *drv_info;
+ struct mtk_md_dev *mdev;
+ struct queue_info *que;
+ struct txq *txq;
+ int ret;
+
+ que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
+ drv_info = cd->cldma_drv_info[que->hif_id];
+ if (unlikely(!drv_info))
+ return -EPIPE;
+ txq = drv_info->txq[que->txqno];
+ if (unlikely(!txq) || txq->is_stopping)
+ return -EPIPE;
+
+ mdev = drv_info->mdev;
+
+ ret = mtk_cldma_start_xfer(drv_info, que->txqno);
+ if (unlikely(ret))
+ dev_err(mdev->dev, "Failed to trigger cldma tx\n");
+
+ return ret;
+}
+
+static int mtk_cldma_close(struct cldma_dev *cd, struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_info *drv_info;
+ struct queue_info *que;
+
+ que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
+ drv_info = cd->cldma_drv_info[que->hif_id];
+ if (unlikely(!drv_info))
+ return -EPIPE;
+
+ if (drv_info->txq[que->txqno])
+ mtk_cldma_txq_free(drv_info, que->txqno);
+ if (drv_info->rxq[que->rxqno])
+ mtk_cldma_rxq_free(drv_info, que->rxqno);
+
+ trb->status = 0;
+ trb->trb_complete(skb);
+
+ return 0;
+}
+
+static int mtk_cldma_txbuf_set(struct cldma_drv_info *drv_info, struct sk_buff *skb,
+ struct tx_req *req, int nr_bds)
+{
+ struct sk_buff *curr_skb, *next_skb;
+ struct mtk_md_dev *mdev;
+ struct bd_dsc *bd_dsc;
+ int ret;
+ int i;
+
+ mdev = drv_info->mdev;
+
+ if (nr_bds) {
+ bd_dsc = req->bd_dsc_pool;
+ curr_skb = skb;
+ for (i = 0; i < nr_bds && curr_skb; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ if (req->bd_dsc_pool == bd_dsc) {
+ bd_dsc->data_len = skb->len - skb->data_len;
+ next_skb = skb_shinfo(skb)->frag_list;
+ } else {
+ bd_dsc->data_len = curr_skb->len;
+ next_skb = curr_skb->next;
+ }
+ bd_dsc->data_dma_addr = dma_map_single(mdev->dev, curr_skb->data,
+ bd_dsc->data_len, DMA_TO_DEVICE);
+ ret = dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr);
+ if (unlikely(ret))
+ goto err_unmap_buffer;
+
+ bd_dsc->bd->tx_bd.data_buff_ptr_h =
+ cpu_to_le32((u64)(bd_dsc->data_dma_addr) >> 32);
+ bd_dsc->bd->tx_bd.data_buff_ptr_l = cpu_to_le32(bd_dsc->data_dma_addr);
+ bd_dsc->bd->tx_bd.data_buffer_len = cpu_to_le16(bd_dsc->data_len);
+ curr_skb = next_skb;
+ }
+ bd_dsc->bd->tx_bd.bd_flags = CLDMA_BD_FLAG_EOL;
+ } else {
+ req->data_dma_addr = dma_map_single(mdev->dev, skb->data,
+ skb_headlen(skb), DMA_TO_DEVICE);
+ ret = dma_mapping_error(mdev->dev, req->data_dma_addr);
+ if (unlikely(ret)) {
+ req->data_dma_addr = 0;
+ goto err_exit;
+ }
+
+ req->gpd->tx_gpd.data_buff_ptr_h = cpu_to_le32((u64)(req->data_dma_addr) >> 32);
+ req->gpd->tx_gpd.data_buff_ptr_l = cpu_to_le32(req->data_dma_addr);
+ }
+
+ return 0;
+
+err_unmap_buffer:
+ for (i = 0; i < nr_bds; i++) {
+ bd_dsc = req->bd_dsc_pool + i;
+ if (dma_mapping_error(mdev->dev, bd_dsc->data_dma_addr)) {
+ bd_dsc->data_dma_addr = 0;
+ break;
+ }
+ dma_unmap_single(mdev->dev, bd_dsc->data_dma_addr,
+ bd_dsc->data_len, DMA_TO_DEVICE);
+ bd_dsc->data_dma_addr = 0;
+ }
+err_exit:
+ dev_err(mdev->dev, "Failed to map dma! error:%d\n", ret);
+ return -EAGAIN;
+}
+
+int mtk_cldma_submit_tx(void *dev, struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct cldma_drv_info *drv_info;
+ struct cldma_dev *cd = dev;
+ struct queue_info *que;
+ struct tx_req *req;
+ struct txq *txq;
+ int ret;
+
+ que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
+ drv_info = cd->cldma_drv_info[que->hif_id];
+ if (unlikely(!drv_info))
+ return -EINVAL;
+
+ txq = drv_info->txq[que->txqno];
+ if (unlikely(!txq))
+ return -EINVAL;
+
+ if (!atomic_read(&txq->req_budget))
+ return -EAGAIN;
+
+ req = txq->req_pool + txq->wr_idx;
+ req->gpd->tx_gpd.debug_id = 0x01;
+ ret = mtk_cldma_txbuf_set(drv_info, skb, req, txq->nr_bds);
+ if (ret)
+ return ret;
+
+ req->gpd->tx_gpd.data_buff_len = cpu_to_le16(skb->len);
+
+ req->data_len = skb->len;
+ req->skb = skb;
+ req->data_vm_addr = skb->data;
+
+ wmb(); /* ensure req and data msg set done before HWO setup */
+
+ req->gpd->tx_gpd.gpd_flags |= CLDMA_GPD_FLAG_HWO;
+
+ wmb(); /* ensure HWO setup done before index update */
+
+ WRITE_ONCE(txq->wr_idx, (txq->wr_idx + 1) % txq->nr_gpds);
+ atomic_dec(&txq->req_budget);
+
+ return 0;
+}
+
+int mtk_cldma_get_tx_budget(void *dev, enum mtk_hif_id hif_id, u32 qno)
+{
+ struct cldma_drv_info *drv_info;
+ struct cldma_dev *cd = dev;
+ struct txq *txq;
+
+ if (unlikely(hif_id >= NR_CLDMA || qno >= HW_QUE_NUM || !cd))
+ return -EINVAL;
+
+ drv_info = cd->cldma_drv_info[hif_id];
+ if (!drv_info)
+ return -EINVAL;
+ txq = drv_info->txq[qno];
+ if (!txq)
+ return -EINVAL;
+ return atomic_read(&txq->req_budget);
+}
+
+static int (*trb_act_tbl[TRB_CMD_MAX])(struct cldma_dev *cd, struct sk_buff *skb) = {
+ [TRB_CMD_ENABLE] = mtk_cldma_open,
+ [TRB_CMD_TX] = mtk_cldma_tx,
+ [TRB_CMD_DISABLE] = mtk_cldma_close,
+};
+
+int mtk_cldma_trb_process(void *dev, struct sk_buff *skb)
+{
+ struct cldma_dev *cd;
+ struct trb *trb;
+
+ if (!dev || !skb)
+ return -EINVAL;
+
+ cd = (struct cldma_dev *)dev;
+ trb = (struct trb *)skb->cb;
+
+ if (!(trb->cmd > TRB_CMD_MIN && trb->cmd < TRB_CMD_STOP))
+ return -EINVAL;
+
+ return trb_act_tbl[trb->cmd](cd, skb);
+}
+
+int mtk_cldma_check_ch_cfg(void *dev, struct queue_info *que)
+{
+ struct cldma_drv_info *drv_info;
+ struct cldma_dev *cd = dev;
+ struct mtk_md_dev *mdev;
+ struct txq *txq;
+ struct rxq *rxq;
+
+ mdev = cd->trans->mdev;
+ drv_info = cd->cldma_drv_info[que->hif_id];
+
+ if (!drv_info) {
+ dev_err(mdev->dev, "CLDMA%d has not been initialized\n",
+ mtk_cldma_hw_id_tbl[que->hif_id]);
+ return -EINVAL;
+ }
+
+ txq = drv_info->txq[que->txqno];
+ rxq = drv_info->rxq[que->rxqno];
+ if (!txq || !rxq) {
+ dev_err(mdev->dev,
+ "CLDMA%d txq%d rxq%d has not been enabled\n",
+ mtk_cldma_hw_id_tbl[que->hif_id], que->txqno, que->rxqno);
+ return -EINVAL;
+ }
+
+ if (que->tx_mtu != txq->que->tx_mtu || que->rx_mtu != rxq->que->rx_mtu) {
+ dev_err(mdev->dev,
+ "Channel:%08x tx_mtu:%08x rx_mtu:%08x do not match ch cfg\n",
+ que->tx_chl, que->tx_mtu, que->rx_mtu);
+ return -EINVAL;
+ }
+
+ return 0;
+}
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.h b/drivers/net/wwan/t9xx/pcie/mtk_cldma.h
new file mode 100644
index 000000000000..4686f7b178e5
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.h
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_CLDMA_H__
+#define __MTK_CLDMA_H__
+
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/interrupt.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+#include "mtk_ctrl_plane.h"
+#include "mtk_trans_ctrl.h"
+
+struct mtk_fsm_param;
+
+#define TXQ(N) (N)
+#define RXQ(N) (N)
+
+#define CLDMA_GPD_FLAG_HWO BIT(0)
+#define CLDMA_GPD_FLAG_BDP BIT(1)
+#define CLDMA_GPD_FLAG_BPS BIT(2)
+#define CLDMA_GPD_FLAG_IOC BIT(7)
+#define CLDMA_BD_FLAG_EOL BIT(0)
+
+union gpd {
+ struct {
+ u8 gpd_flags;
+ u8 non_used1;
+ __le16 data_allow_len;
+ __le32 next_gpd_ptr_h;
+ __le32 next_gpd_ptr_l;
+ __le32 data_buff_ptr_h;
+ __le32 data_buff_ptr_l;
+ __le16 data_recv_len;
+ u8 non_used2;
+ u8 debug_id;
+ } rx_gpd;
+
+ struct {
+ u8 gpd_flags;
+ u8 non_used1;
+ u8 non_used2;
+ u8 debug_id;
+ __le32 next_gpd_ptr_h;
+ __le32 next_gpd_ptr_l;
+ __le32 data_buff_ptr_h;
+ __le32 data_buff_ptr_l;
+ __le16 data_buff_len;
+ __le16 non_used3;
+ } tx_gpd;
+} __packed;
+
+union bd {
+ struct {
+ u8 bd_flags;
+ u8 non_used1;
+ __le16 data_allow_len;
+ __le32 next_bd_ptr_h;
+ __le32 next_bd_ptr_l;
+ __le32 data_buff_ptr_h;
+ __le32 data_buff_ptr_l;
+ __le16 data_recv_len;
+ __le16 non_used2;
+ } rx_bd;
+
+ struct {
+ u8 bd_flags;
+ u8 non_used1;
+ __le16 non_used2;
+ __le32 next_bd_ptr_h;
+ __le32 next_bd_ptr_l;
+ __le32 data_buff_ptr_h;
+ __le32 data_buff_ptr_l;
+ __le16 data_buffer_len;
+ u8 extension_len;
+ u8 non_used3;
+ } tx_bd;
+} __packed;
+
+struct bd_dsc {
+ union bd *bd;
+ struct sk_buff *skb;
+ dma_addr_t bd_dma_addr;
+ dma_addr_t data_dma_addr;
+ size_t data_len;
+};
+
+struct rx_req {
+ union gpd *gpd;
+ u32 mtu;
+ struct sk_buff *skb;
+ size_t data_len;
+ dma_addr_t gpd_dma_addr;
+ dma_addr_t data_dma_addr;
+ u32 frag_size;
+ struct bd_dsc *bd_dsc_pool;
+};
+
+struct rxq {
+ struct cldma_drv_info *drv_info;
+ u32 rxqno;
+ struct queue_info *que;
+ struct work_struct rx_done_work;
+ struct rx_req *req_pool;
+ u32 nr_gpds;
+ u32 free_idx;
+ unsigned short rx_done_cnt;
+ void *arg;
+ int (*rx_done)(struct sk_buff *skb, void *priv, bool force_recv);
+ u32 nr_bds;
+ atomic_t need_exit;
+};
+
+struct tx_req {
+ union gpd *gpd;
+ u32 mtu;
+ void *data_vm_addr;
+ size_t data_len;
+ dma_addr_t data_dma_addr;
+ dma_addr_t gpd_dma_addr;
+ struct sk_buff *skb;
+ int (*trb_complete)(struct sk_buff *skb);
+ u32 frag_size;
+ struct bd_dsc *bd_dsc_pool;
+};
+
+struct txq {
+ struct cldma_drv_info *drv_info;
+ u32 txqno;
+ struct queue_info *que;
+ struct work_struct tx_done_work;
+ struct tx_req *req_pool;
+ u32 nr_gpds;
+ atomic_t req_budget;
+ u32 wr_idx;
+ u32 free_idx;
+ bool tx_started;
+ bool is_stopping;
+ unsigned short tx_done_cnt;
+ u32 nr_bds;
+};
+
+struct cldma_dev {
+ struct cldma_drv_info *cldma_drv_info[NR_CLDMA];
+ struct mtk_ctrl_trans *trans;
+};
+
+struct cldma_drv_info_desc {
+ u32 hw_ver;
+ struct cldma_drv_ops *drv_ops;
+ struct cldma_hw_regs *hw_regs;
+};
+
+int mtk_cldma_init(struct mtk_ctrl_trans *trans);
+void mtk_cldma_exit(struct mtk_ctrl_trans *trans);
+int mtk_cldma_submit_tx(void *dev, struct sk_buff *skb);
+int mtk_cldma_get_tx_budget(void *dev, enum mtk_hif_id hif_id, u32 qno);
+int mtk_cldma_trb_process(void *dev, struct sk_buff *skb);
+void mtk_cldma_fsm_state_listener(struct mtk_fsm_param *param, struct mtk_ctrl_trans *trans);
+int mtk_cldma_check_ch_cfg(void *dev, struct queue_info *que);
+
+#define drv_ops_name(NAME) cldma_drv_ops_##NAME
+#define cldma_regs_name(NAME) mtk_cldma_regs_##NAME
+
+extern struct cldma_drv_ops cldma_drv_ops_m9xx;
+extern struct cldma_hw_regs mtk_cldma_regs_m9xx;
+
+#endif
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c
new file mode 100644
index 000000000000..8745c1470891
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2023, MediaTek Inc.
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/iopoll.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kdev_t.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/netdevice.h>
+#include <linux/sched.h>
+#include <linux/skbuff.h>
+#include <linux/slab.h>
+#include <linux/timer.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+
+#include "mtk_cldma_drv.h"
+#include "mtk_dev.h"
+#include "mtk_pci.h"
+#include "mtk_pci_reg.h"
+
+#define WAIT_QUEUE_STOP (70)
+
+void mtk_cldma_drv_init(struct cldma_drv_info *drv_info)
+{
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ int base;
+ u32 val;
+
+ mdev = drv_info->mdev;
+ base = drv_info->base_addr;
+ hw_regs = drv_info->hw_regs;
+
+ /* set CLDMA to 64 bit mode GPD */
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_ul_cfg);
+ val = (val & (~(0x7 << 5))) | ((0x4) << 5);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ul_cfg, val);
+
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_so_cfg);
+ val = (val & (~(0x7 << 10))) | ((0x4) << 10) | (1 << 2);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_so_cfg, val);
+
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_rx_work_to_reg_mask_set, ALLQ);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ip_busy_to_pcie_mask_set,
+ ALLQ << 16);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ip_busy_to_pcie_mask_clr,
+ ALLQ << 24);
+
+ /* enable interrupt to PCIe */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_int_mask, 0);
+
+ /* disable illegal memory check */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ul_dummy_0, 1);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_so_dummy_0, 1);
+}
+
+void mtk_cldma_setup_start_addr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, dma_addr_t addr)
+{
+ struct cldma_hw_regs *hw_regs;
+ unsigned int addr_l;
+ unsigned int addr_h;
+ int base;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX) {
+ addr_l = base + hw_regs->reg_cldma_ul_start_addrl_0 + qno * HW_QUEUE_NUM;
+ addr_h = base + hw_regs->reg_cldma_ul_start_addrh_0 + qno * HW_QUEUE_NUM;
+ } else {
+ addr_l = base + hw_regs->reg_cldma_so_start_addrl_0 + qno * HW_QUEUE_NUM;
+ addr_h = base + hw_regs->reg_cldma_so_start_addrh_0 + qno * HW_QUEUE_NUM;
+ }
+
+ mtk_pci_write32(drv_info->mdev, addr_l, (u32)addr);
+ mtk_pci_write32(drv_info->mdev, addr_h, (u32)((u64)addr >> 32));
+}
+
+void mtk_cldma_mask_intr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type)
+{
+ struct cldma_hw_regs *hw_regs;
+ int base;
+ u32 addr;
+ u32 val;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_l2timsr0;
+ else
+ addr = base + hw_regs->reg_cldma_l2rimsr0;
+
+ if (qno == ALLQ)
+ val = qno << type;
+ else
+ val = BIT(qno) << type;
+
+ mtk_pci_write32(drv_info->mdev, addr, val);
+}
+
+void mtk_cldma_unmask_intr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type)
+{
+ struct cldma_hw_regs *hw_regs;
+ int base;
+ u32 addr;
+ u32 val;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_l2timcr0;
+ else
+ addr = base + hw_regs->reg_cldma_l2rimcr0;
+
+ if (qno == ALLQ)
+ val = qno << type;
+ else
+ val = BIT(qno) << type;
+
+ mtk_pci_write32(drv_info->mdev, addr, val);
+}
+
+void mtk_cldma_clr_intr_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type)
+{
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ int base;
+ u32 addr;
+ u32 val;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+ mdev = drv_info->mdev;
+
+ if (type == QUEUE_ERROR) {
+ if (dir == DIR_TX) {
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l3tisar0);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l3tisar0, val);
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l3tisar1);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l3tisar1, val);
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l3tisar2);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l3tisar2, val);
+ } else {
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l3risar0);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l3risar0, val);
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l3risar1);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l3risar1, val);
+ }
+ }
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_l2tisar0;
+ else
+ addr = base + hw_regs->reg_cldma_l2risar0;
+
+ if (qno == ALLQ)
+ val = qno << type;
+ else
+ val = BIT(qno) << type;
+
+ mtk_pci_write32(mdev, addr, val);
+ val = mtk_pci_read32(mdev, addr);
+}
+
+u32 mtk_cldma_check_intr_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type)
+{
+ struct cldma_hw_regs *hw_regs;
+ u32 addr, val, sta;
+ int base;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_l2tisar0;
+ else
+ addr = base + hw_regs->reg_cldma_l2risar0;
+
+ val = mtk_pci_read32(drv_info->mdev, addr);
+ if (val == LINK_ERROR_VAL)
+ sta = val;
+ else if (qno == ALLQ)
+ sta = (val >> type) & 0xFF;
+ else
+ sta = (val >> type) & BIT(qno);
+
+ return sta;
+}
+
+void mtk_cldma_start_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno)
+{
+ struct cldma_hw_regs *hw_regs;
+ u32 val = BIT(qno);
+ int base;
+ u32 addr;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_ul_start_cmd;
+ else
+ addr = base + hw_regs->reg_cldma_so_start_cmd;
+
+ mtk_pci_write32(drv_info->mdev, addr, val);
+}
+
+void mtk_cldma_resume_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno)
+{
+ struct cldma_hw_regs *hw_regs;
+ u32 val = BIT(qno);
+ int base;
+ u32 addr;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_ul_resume_cmd;
+ else
+ addr = base + hw_regs->reg_cldma_so_resume_cmd;
+
+ mtk_pci_write32(drv_info->mdev, addr, val);
+}
+
+u32 mtk_cldma_queue_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno)
+{
+ struct cldma_hw_regs *hw_regs;
+ int base;
+ u32 addr;
+ u32 val;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_ul_status;
+ else
+ addr = base + hw_regs->reg_cldma_so_status;
+
+ val = mtk_pci_read32(drv_info->mdev, addr);
+
+ if (qno == ALLQ || val == LINK_ERROR_VAL)
+ return val;
+
+ return val & BIT(qno);
+}
+
+u32 mtk_cldma_stop_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno)
+{
+ u32 val = (qno == ALLQ) ? qno : BIT(qno);
+ struct cldma_hw_regs *hw_regs;
+ unsigned int active;
+ int base;
+ u32 addr;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+
+ if (dir == DIR_TX)
+ addr = base + hw_regs->reg_cldma_ul_stop_cmd;
+ else
+ addr = base + hw_regs->reg_cldma_so_stop_cmd;
+
+ mtk_pci_write32(drv_info->mdev, addr, val);
+
+ read_poll_timeout(mtk_cldma_queue_status, active,
+ active == LINK_ERROR_VAL || !active,
+ WAIT_QUEUE_STOP, WAIT_QUEUE_STOP * 10, false,
+ drv_info, dir, qno);
+
+ return active;
+}
+
+void mtk_cldma_clear_ip_busy(struct cldma_drv_info *drv_info)
+{
+ mtk_pci_write32(drv_info->mdev, drv_info->base_addr +
+ drv_info->hw_regs->reg_cldma_ip_busy, 0x01);
+}
+
+void mtk_cldma_get_intr_status(struct cldma_drv_info *drv_info, u32 *tx_sta, u32 *rx_sta)
+{
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ u32 tx_mask, rx_mask;
+ int base;
+
+ mdev = drv_info->mdev;
+ base = drv_info->base_addr;
+ hw_regs = drv_info->hw_regs;
+
+ *tx_sta = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l2tisar0);
+ tx_mask = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l2timr0);
+ *rx_sta = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l2risar0);
+ rx_mask = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_l2rimr0);
+
+ *tx_sta = (*tx_sta) & (~tx_mask);
+ *rx_sta = (*rx_sta) & (~rx_mask);
+
+ if (*tx_sta) {
+ /* TX XFER_DONE and QUEUE_ERROR mask */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2timsr0, *tx_sta);
+ /* TX XFER_DONE clear */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2tisar0,
+ (*tx_sta) & (0xFF << QUEUE_XFER_DONE));
+ }
+
+ if (*rx_sta) {
+ /* RX XFER_DONE and QUEUE_ERROR mask */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2rimsr0, *rx_sta);
+ /* RX XFER_DONE clear */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2risar0,
+ (*rx_sta) & (0xFF << QUEUE_XFER_DONE));
+ }
+}
+
+u32 mtk_cldma_get_tx_start_addr(struct cldma_drv_info *drv_info, u32 qno)
+{
+ u32 addr, val;
+
+ addr = drv_info->base_addr + drv_info->hw_regs->reg_cldma_ul_start_addrl_0 +
+ qno * HW_QUEUE_NUM;
+ val = mtk_pci_read32(drv_info->mdev, addr);
+
+ return val;
+}
+
+u64 mtk_cldma_get_rx_curr_addr(struct cldma_drv_info *drv_info, u32 qno)
+{
+ struct cldma_hw_regs *hw_regs;
+ u32 curr_addr_h, curr_addr_l;
+ struct mtk_md_dev *mdev;
+ u64 curr_addr;
+ int base;
+ u64 addr;
+
+ hw_regs = drv_info->hw_regs;
+ base = drv_info->base_addr;
+ mdev = drv_info->mdev;
+
+ addr = base + hw_regs->reg_cldma_so_current_addrh_0 +
+ (u64)qno * HW_QUEUE_NUM;
+ curr_addr_h = mtk_pci_read32(mdev, addr);
+ addr = base + hw_regs->reg_cldma_so_current_addrl_0 +
+ (u64)qno * HW_QUEUE_NUM;
+ curr_addr_l = mtk_pci_read32(mdev, addr);
+ curr_addr = ((u64)curr_addr_h << 32) | curr_addr_l;
+ if (curr_addr_h == LINK_ERROR_VAL && curr_addr_l == LINK_ERROR_VAL)
+ curr_addr = 0;
+ return curr_addr;
+}
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h
new file mode 100644
index 000000000000..8763c23abf54
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2023, MediaTek Inc.
+ */
+
+#ifndef __MTK_CLDMA_DRV_H__
+#define __MTK_CLDMA_DRV_H__
+
+#define HW_QUEUE_NUM (8)
+#define ALLQ (0xFF)
+#define LINK_ERROR_VAL (0xFFFFFFFF)
+#define CLDMA0_HW_ID (0)
+#define CLDMA1_HW_ID (1)
+#define CLDMA4_HW_ID (4)
+
+struct cldma_hw_regs {
+ u8 cldma_rx_skb_pool_max_size;
+ u8 cldma_rx_skb_reload_threshold;
+ u8 tq_err_int_offset;
+ u8 tq_active_start_err_int_offset;
+ u8 rq_err_int_offset;
+ u8 rq_active_start_err_int_offset;
+ u16 reg_cldma_so_cfg;
+ u16 reg_cldma_so_start_addrl_0;
+ u16 reg_cldma_so_start_addrh_0;
+ u16 reg_cldma_so_current_addrl_0;
+ u16 reg_cldma_so_current_addrh_0;
+ u16 reg_cldma_so_status;
+ u16 reg_cldma_debug_id_en;
+ u16 reg_cldma_so_last_update_addrl_0;
+ u16 reg_cldma_so_last_update_addrh_0;
+ u16 reg_cldma_l2rimr0;
+ u16 reg_cldma_l2rimr1;
+ u16 reg_cldma_l2rimcr0;
+ u16 reg_cldma_l2rimcr1;
+ u16 reg_cldma_l2rimsr0;
+ u16 reg_cldma_l2rimsr1;
+ u16 reg_cldma_int_mask;
+ u16 reg_cldma4_int_mask;
+ u16 reg_cldma_slp_mem_ctl;
+ u16 reg_cldma_busy_mask;
+ u16 reg_cldma_ip_busy_to_pcie_mask;
+ u16 reg_cldma_ip_busy_to_pcie_mask_set;
+ u16 reg_cldma_ip_busy_to_pcie_mask_clr;
+ u16 reg_cldma_ip_busy_to_ap_mask;
+ u16 reg_cldma_ip_busy_to_ap_mask_set;
+ u16 reg_cldma_ip_busy_to_ap_mask_clr;
+ u16 reg_cldma_ip_busy_to_md_mask_set;
+ u16 reg_cldma_rx_work_to_reg_mask_set;
+ u16 reg_infra_rst4_set;
+ u16 reg_infra_rst4_clr;
+ u16 reg_infra_rst2_set;
+ u16 reg_infra_rst2_clr;
+ u16 reg_infra_rst0_set;
+ u16 reg_infra_rst0_clr;
+ u32 tq_err_int_bitmask;
+ u32 tq_active_start_err_int_bitmask;
+ u32 rq_err_int_bitmask;
+ u32 cldma0_base_addr;
+ u32 cldma1_base_addr;
+ u32 cldma4_base_addr;
+ u32 rq_active_start_err_int_bitmask;
+ u32 reg_cldma_ul_start_addrl_0;
+ u32 reg_cldma_ul_start_addrh_0;
+ u32 reg_cldma_ul_current_addrl_0;
+ u32 reg_cldma_ul_current_addrh_0;
+ u32 reg_cldma_ul_status;
+ u32 reg_cldma_ul_start_cmd;
+ u32 reg_cldma_ul_resume_cmd;
+ u32 reg_cldma_ul_stop_cmd;
+ u32 reg_cldma_ul_error;
+ u32 reg_cldma_ul_cfg;
+ u32 reg_cldma_ul_dummy_0;
+ u32 reg_cldma_so_error;
+ u32 reg_cldma_so_start_cmd;
+ u32 reg_cldma_so_resume_cmd;
+ u32 reg_cldma_so_stop_cmd;
+ u32 reg_cldma_so_dummy_0;
+ u32 reg_cldma_l2tisar0;
+ u32 reg_cldma_l2tisar1;
+ u32 reg_cldma_l2timr0;
+ u32 reg_cldma_l2timr1;
+ u32 reg_cldma_l2timcr0;
+ u32 reg_cldma_l2timcr1;
+ u32 reg_cldma_l2timsr0;
+ u32 reg_cldma_l2timsr1;
+ u32 reg_cldma_l2risar0;
+ u32 reg_cldma_l2risar1;
+ u32 reg_cldma_l3tisar0;
+ u32 reg_cldma_l3tisar1;
+ u32 reg_cldma_l3tisar2;
+ u32 reg_cldma_l3risar0;
+ u32 reg_cldma_l3risar1;
+ u32 reg_cldma_ip_busy;
+};
+
+enum mtk_ip_busy_src {
+ IP_BUSY_TXDONE = 0,
+ IP_BUSY_TXEMPTY = 8,
+ IP_BUSY_TXACTIVE = 16,
+ IP_BUSY_RXDONE = 24
+};
+
+enum mtk_intr_type {
+ QUEUE_XFER_DONE = 0,
+ QUEUE_EMPTY = 8,
+ QUEUE_ERROR = 16,
+ QUEUE_ACTIVE_START = 24,
+ INVALID_TYPE
+};
+
+enum mtk_tx_rx {
+ DIR_TX,
+ DIR_RX,
+ DIR_MAX
+};
+
+struct cldma_drv_info {
+ int hif_id;
+ int hw_id;
+ int base_addr;
+ int pci_ext_irq_id;
+ struct mtk_md_dev *mdev;
+ struct cldma_dev *cd;
+ struct txq *txq[HW_QUEUE_NUM];
+ struct rxq *rxq[HW_QUEUE_NUM];
+ struct dma_pool *gpd_dma_pool;
+ struct dma_pool *bd_dma_pool;
+ struct workqueue_struct *wq;
+ struct cldma_hw_regs *hw_regs;
+ struct cldma_drv_ops *drv_ops;
+};
+
+struct cldma_drv_ops {
+ void (*cldma_drv_init)(struct cldma_drv_info *drv_info);
+ void (*cldma_drv_reset)(struct cldma_drv_info *drv_info);
+ void (*cldma_setup_start_addr)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, dma_addr_t addr);
+ void (*cldma_mask_intr)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+ void (*cldma_unmask_intr)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+ void (*cldma_clr_intr_status)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+ u32 (*cldma_check_intr_status)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+ void (*cldma_start_queue)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+ void (*cldma_resume_queue)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+ u32 (*cldma_queue_status)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+ u32 (*cldma_stop_queue)(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+ void (*cldma_clear_ip_busy)(struct cldma_drv_info *drv_info);
+ void (*cldma_get_intr_status)(struct cldma_drv_info *drv_info, u32 *tx_sta, u32 *rx_sta);
+ u32 (*cldma_get_tx_start_addr)(struct cldma_drv_info *drv_info, u32 qno);
+ u64 (*cldma_get_rx_curr_addr)(struct cldma_drv_info *drv_info, u32 qno);
+};
+
+void mtk_cldma_drv_init(struct cldma_drv_info *drv_info);
+void mtk_cldma_setup_start_addr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, dma_addr_t addr);
+void mtk_cldma_mask_intr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+void mtk_cldma_unmask_intr(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+void mtk_cldma_clr_intr_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+u32 mtk_cldma_check_intr_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir,
+ u32 qno, enum mtk_intr_type type);
+void mtk_cldma_start_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+void mtk_cldma_resume_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+u32 mtk_cldma_queue_status(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+u32 mtk_cldma_stop_queue(struct cldma_drv_info *drv_info, enum mtk_tx_rx dir, u32 qno);
+void mtk_cldma_clear_ip_busy(struct cldma_drv_info *drv_info);
+void mtk_cldma_get_intr_status(struct cldma_drv_info *drv_info, u32 *tx_sta, u32 *rx_sta);
+u32 mtk_cldma_get_tx_start_addr(struct cldma_drv_info *drv_info, u32 qno);
+u64 mtk_cldma_get_rx_curr_addr(struct cldma_drv_info *drv_info, u32 qno);
+
+#endif
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c
new file mode 100644
index 000000000000..d9145d146a5c
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2023, MediaTek Inc.
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kdev_t.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/netdevice.h>
+#include <linux/sched.h>
+#include <linux/skbuff.h>
+#include <linux/slab.h>
+#include <linux/timer.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+
+#include "mtk_cldma.h"
+#include "mtk_cldma_drv.h"
+#include "mtk_cldma_drv_m9xx.h"
+#include "mtk_dev.h"
+#include "mtk_pci.h"
+#include "mtk_pci_reg.h"
+#include "mtk_trans_ctrl.h"
+
+struct cldma_hw_regs mtk_cldma_regs_m9xx = {
+ .cldma0_base_addr = CLDMA0_BASE_ADDR,
+ .cldma1_base_addr = CLDMA1_BASE_ADDR,
+ .cldma4_base_addr = CLDMA4_BASE_ADDR,
+ .cldma_rx_skb_pool_max_size = CLDMA_RX_SKB_POOL_MAX_SIZE,
+ .cldma_rx_skb_reload_threshold = CLDMA_RX_SKB_RELOAD_THRESHOLD,
+ .tq_err_int_offset = TQ_ERR_INT_OFFSET,
+ .tq_err_int_bitmask = TQ_ERR_INT_BITMASK,
+ .tq_active_start_err_int_offset = TQ_ACTIVE_START_ERR_INT_OFFSET,
+ .tq_active_start_err_int_bitmask = TQ_ACTIVE_START_ERR_INT_BITMASK,
+ .rq_err_int_offset = RQ_ERR_INT_OFFSET,
+ .rq_err_int_bitmask = RQ_ERR_INT_BITMASK,
+ .rq_active_start_err_int_offset = RQ_ACTIVE_START_ERR_INT_OFFSET,
+ .rq_active_start_err_int_bitmask = RQ_ACTIVE_START_ERR_INT_BITMASK,
+ .reg_cldma_ul_start_addrl_0 = REG_CLDMA_UL_START_ADDRL_0,
+ .reg_cldma_ul_start_addrh_0 = REG_CLDMA_UL_START_ADDRH_0,
+ .reg_cldma_ul_current_addrl_0 = REG_CLDMA_UL_CURRENT_ADDRL_0,
+ .reg_cldma_ul_current_addrh_0 = REG_CLDMA_UL_CURRENT_ADDRH_0,
+ .reg_cldma_ul_status = REG_CLDMA_UL_STATUS,
+ .reg_cldma_ul_start_cmd = REG_CLDMA_UL_START_CMD,
+ .reg_cldma_ul_resume_cmd = REG_CLDMA_UL_RESUME_CMD,
+ .reg_cldma_ul_stop_cmd = REG_CLDMA_UL_STOP_CMD,
+ .reg_cldma_ul_error = REG_CLDMA_UL_ERROR,
+ .reg_cldma_ul_cfg = REG_CLDMA_UL_CFG,
+ .reg_cldma_ul_dummy_0 = REG_CLDMA_UL_DUMMY_0,
+ .reg_cldma_so_error = REG_CLDMA_SO_ERROR,
+ .reg_cldma_so_start_cmd = REG_CLDMA_SO_START_CMD,
+ .reg_cldma_so_resume_cmd = REG_CLDMA_SO_RESUME_CMD,
+ .reg_cldma_so_stop_cmd = REG_CLDMA_SO_STOP_CMD,
+ .reg_cldma_so_dummy_0 = REG_CLDMA_SO_DUMMY_0,
+ .reg_cldma_so_cfg = REG_CLDMA_SO_CFG,
+ .reg_cldma_so_start_addrl_0 = REG_CLDMA_SO_START_ADDRL_0,
+ .reg_cldma_so_start_addrh_0 = REG_CLDMA_SO_START_ADDRH_0,
+ .reg_cldma_so_current_addrl_0 = REG_CLDMA_SO_CUR_ADDRL_0,
+ .reg_cldma_so_current_addrh_0 = REG_CLDMA_SO_CUR_ADDRH_0,
+ .reg_cldma_so_status = REG_CLDMA_SO_STATUS,
+ .reg_cldma_debug_id_en = REG_CLDMA_DEBUG_ID_EN,
+ .reg_cldma_so_last_update_addrl_0 = REG_CLDMA_SO_LAST_UPDATE_ADDRL_0,
+ .reg_cldma_so_last_update_addrh_0 = REG_CLDMA_SO_LAST_UPDATE_ADDRH_0,
+ .reg_cldma_l2tisar0 = REG_CLDMA_L2TISAR0,
+ .reg_cldma_l2tisar1 = REG_CLDMA_L2TISAR1,
+ .reg_cldma_l2timr0 = REG_CLDMA_L2TIMR0,
+ .reg_cldma_l2timr1 = REG_CLDMA_L2TIMR1,
+ .reg_cldma_l2timcr0 = REG_CLDMA_L2TIMCR0,
+ .reg_cldma_l2timcr1 = REG_CLDMA_L2TIMCR1,
+ .reg_cldma_l2timsr0 = REG_CLDMA_L2TIMSR0,
+ .reg_cldma_l2timsr1 = REG_CLDMA_L2TIMSR1,
+ .reg_cldma_l3tisar0 = REG_CLDMA_L3TISAR0,
+ .reg_cldma_l3tisar1 = REG_CLDMA_L3TISAR1,
+ .reg_cldma_l3tisar2 = REG_CLDMA_L3TISAR2,
+ .reg_cldma_l2risar0 = REG_CLDMA_L2RISAR0,
+ .reg_cldma_l2risar1 = REG_CLDMA_L2RISAR1,
+ .reg_cldma_l2rimr0 = REG_CLDMA_L2RIMR0,
+ .reg_cldma_l2rimr1 = REG_CLDMA_L2RIMR1,
+ .reg_cldma_l2rimcr0 = REG_CLDMA_L2RIMCR0,
+ .reg_cldma_l2rimcr1 = REG_CLDMA_L2RIMCR1,
+ .reg_cldma_l2rimsr0 = REG_CLDMA_L2RIMSR0,
+ .reg_cldma_l2rimsr1 = REG_CLDMA_L2RIMSR1,
+ .reg_cldma_l3risar0 = REG_CLDMA_L3RISAR0,
+ .reg_cldma_l3risar1 = REG_CLDMA_L3RISAR1,
+ .reg_cldma_ip_busy = REG_CLDMA_IP_BUSY,
+ .reg_cldma_int_mask = REG_CLDMA_INT_EAP_USIP_MASK,
+ .reg_cldma4_int_mask = REG_CLDMA_INT_WF_MASK,
+ .reg_cldma_ip_busy_to_pcie_mask = REG_CLDMA_IP_BUSY_TO_PCIE_MASK,
+ .reg_cldma_ip_busy_to_pcie_mask_set = REG_CLDMA_IP_BUSY_TO_PCIE_MASK_SET,
+ .reg_cldma_ip_busy_to_pcie_mask_clr = REG_CLDMA_IP_BUSY_TO_PCIE_MASK_CLR,
+ .reg_cldma_ip_busy_to_ap_mask = REG_CLDMA_IP_BUSY_TO_AP_MASK,
+ .reg_cldma_ip_busy_to_ap_mask_set = REG_CLDMA_IP_BUSY_TO_AP_MASK_SET,
+ .reg_cldma_ip_busy_to_ap_mask_clr = REG_CLDMA_IP_BUSY_TO_AP_MASK_CLR,
+ .reg_cldma_ip_busy_to_md_mask_set = REG_CLDMA_IP_BUSY_TO_MD_MASK_SET,
+ .reg_cldma_rx_work_to_reg_mask_set = REG_CLDMA_RX_WORK_TO_REG_MASK_SET,
+ .reg_infra_rst0_set = REG_INFRA_RST0_SET,
+ .reg_infra_rst0_clr = REG_INFRA_RST0_CLR,
+};
+
+static void mtk_cldma_drv_init_m9xx(struct cldma_drv_info *drv_info)
+{
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ int base;
+ u32 val;
+
+ mdev = drv_info->mdev;
+ base = drv_info->base_addr;
+ hw_regs = drv_info->hw_regs;
+
+ /* set CLDMA to 64 bit mode GPD */
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_ul_cfg);
+
+ val = (val & (~(0x7 << 5))) | ((0x4) << 5);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ul_cfg, val);
+
+ val = mtk_pci_read32(mdev, base + hw_regs->reg_cldma_so_cfg);
+ val = (val & (~(0x7 << 10))) | ((0x4) << 10) | (1 << 2);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_so_cfg, val);
+
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_rx_work_to_reg_mask_set, ALLQ);
+
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ip_busy_to_pcie_mask_set,
+ ALLQ << 16);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ip_busy_to_pcie_mask_clr,
+ ALLQ << 24);
+
+ /* enable interrupt to PCIe */
+ if (drv_info->hw_id == CLDMA4_HW_ID)
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma4_int_mask, 0);
+ else
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_int_mask, 0);
+
+ /* disable illegal memory check */
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ul_dummy_0, 1);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_so_dummy_0, 1);
+}
+
+static void mtk_cldma_drv_reset_m9xx(struct cldma_drv_info *drv_info)
+{
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ u32 val;
+
+ mdev = drv_info->mdev;
+ hw_regs = drv_info->hw_regs;
+
+ val = mtk_pci_read32(mdev, REG_DEV_INFRA_BASE + hw_regs->reg_infra_rst0_set);
+
+ val |= 1 << (REG_CLDMA0_RST_SET_BIT + drv_info->hw_id);
+ mtk_pci_write32(mdev, REG_DEV_INFRA_BASE + hw_regs->reg_infra_rst0_set, val);
+ udelay(1);
+ val = mtk_pci_read32(mdev, REG_DEV_INFRA_BASE + hw_regs->reg_infra_rst0_clr);
+ val |= 1 << (REG_CLDMA0_RST_CLR_BIT + drv_info->hw_id);
+ mtk_pci_write32(mdev, REG_DEV_INFRA_BASE + hw_regs->reg_infra_rst0_clr, val);
+}
+
+struct cldma_drv_ops cldma_drv_ops_m9xx = {
+ .cldma_drv_init = mtk_cldma_drv_init_m9xx,
+ .cldma_drv_reset = mtk_cldma_drv_reset_m9xx,
+ .cldma_setup_start_addr = mtk_cldma_setup_start_addr,
+ .cldma_mask_intr = mtk_cldma_mask_intr,
+ .cldma_unmask_intr = mtk_cldma_unmask_intr,
+ .cldma_clr_intr_status = mtk_cldma_clr_intr_status,
+ .cldma_check_intr_status = mtk_cldma_check_intr_status,
+ .cldma_start_queue = mtk_cldma_start_queue,
+ .cldma_resume_queue = mtk_cldma_resume_queue,
+ .cldma_queue_status = mtk_cldma_queue_status,
+ .cldma_stop_queue = mtk_cldma_stop_queue,
+ .cldma_clear_ip_busy = mtk_cldma_clear_ip_busy,
+ .cldma_get_intr_status = mtk_cldma_get_intr_status,
+ .cldma_get_tx_start_addr = mtk_cldma_get_tx_start_addr,
+ .cldma_get_rx_curr_addr = mtk_cldma_get_rx_curr_addr,
+};
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h
new file mode 100644
index 000000000000..2c63c43ff065
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h
@@ -0,0 +1,103 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2023, MediaTek Inc.
+ */
+
+#ifndef __MTK_CLDMA_DRV_M9XX_H__
+#define __MTK_CLDMA_DRV_M9XX_H__
+
+#define CLDMA0_BASE_ADDR (0x1021C000)
+#define CLDMA1_BASE_ADDR (0x1021E000)
+#define CLDMA4_BASE_ADDR (0x10224000)
+
+#define CLDMA_RX_SKB_POOL_MAX_SIZE (64)
+#define CLDMA_RX_SKB_RELOAD_THRESHOLD (16)
+
+/* L2TISAR0 */
+#define TQ_ERR_INT_OFFSET (16)
+#define TQ_ERR_INT_BITMASK (0x00FF0000)
+#define TQ_ACTIVE_START_ERR_INT_OFFSET (24)
+#define TQ_ACTIVE_START_ERR_INT_BITMASK (0xFF000000)
+
+/* L2RISAR0 */
+#define RQ_ERR_INT_OFFSET (16)
+#define RQ_ERR_INT_BITMASK (0x00FF0000)
+#define RQ_ACTIVE_START_ERR_INT_OFFSET (24)
+#define RQ_ACTIVE_START_ERR_INT_BITMASK (0xFF000000)
+
+/* CLDMA IN(Tx) */
+#define REG_CLDMA_UL_START_ADDRL_0 (0x0004)
+#define REG_CLDMA_UL_START_ADDRH_0 (0x0008)
+#define REG_CLDMA_UL_CURRENT_ADDRL_0 (0x0044)
+#define REG_CLDMA_UL_CURRENT_ADDRH_0 (0x0048)
+#define REG_CLDMA_UL_STATUS (0x0084)
+#define REG_CLDMA_UL_START_CMD (0x0088)
+#define REG_CLDMA_UL_RESUME_CMD (0x008C)
+#define REG_CLDMA_UL_STOP_CMD (0x0090)
+#define REG_CLDMA_UL_ERROR (0x0094)
+#define REG_CLDMA_UL_CFG (0x0098)
+#define REG_CLDMA_UL_DUMMY_0 (0x009C)
+
+/* CLDMA OUT(Rx) */
+#define REG_CLDMA_SO_ERROR (0x0400 + 0x0100)
+#define REG_CLDMA_SO_START_CMD (0x0400 + 0x01BC)
+#define REG_CLDMA_SO_RESUME_CMD (0x0400 + 0x01C0)
+#define REG_CLDMA_SO_STOP_CMD (0x0400 + 0x01C4)
+#define REG_CLDMA_SO_DUMMY_0 (0x0400 + 0x0108)
+#define REG_CLDMA_SO_CFG (0x0400 + 0x0004)
+#define REG_CLDMA_SO_START_ADDRL_0 (0x0400 + 0x0078)
+#define REG_CLDMA_SO_START_ADDRH_0 (0x0400 + 0x007C)
+#define REG_CLDMA_SO_CUR_ADDRL_0 (0x0400 + 0x00B8)
+#define REG_CLDMA_SO_CUR_ADDRH_0 (0x0400 + 0x00BC)
+#define REG_CLDMA_SO_STATUS (0x0400 + 0x00F8)
+#define REG_CLDMA_DEBUG_ID_EN (0x0400 + 0x00FC)
+#define REG_CLDMA_SO_LAST_UPDATE_ADDRL_0 (0x0400 + 0x01C8)
+#define REG_CLDMA_SO_LAST_UPDATE_ADDRH_0 (0x0400 + 0x01CC)
+
+/* CLDMA MISC */
+#define REG_CLDMA_L2TISAR0 (0x0800 + 0x0010)
+#define REG_CLDMA_L2TISAR1 (0x0800 + 0x0014)
+#define REG_CLDMA_L2TIMR0 (0x0800 + 0x0018)
+#define REG_CLDMA_L2TIMR1 (0x0800 + 0x001C)
+#define REG_CLDMA_L2TIMCR0 (0x0800 + 0x0020)
+#define REG_CLDMA_L2TIMCR1 (0x0800 + 0x0024)
+#define REG_CLDMA_L2TIMSR0 (0x0800 + 0x0028)
+#define REG_CLDMA_L2TIMSR1 (0x0800 + 0x002C)
+#define REG_CLDMA_L3TISAR0 (0x0800 + 0x0030)
+#define REG_CLDMA_L3TISAR1 (0x0800 + 0x0034)
+#define REG_CLDMA_L2RISAR0 (0x0800 + 0x0050)
+#define REG_CLDMA_L2RISAR1 (0x0800 + 0x0054)
+#define REG_CLDMA_L3RISAR0 (0x0800 + 0x0070)
+#define REG_CLDMA_L3RISAR1 (0x0800 + 0x0074)
+#define REG_CLDMA_IP_BUSY (0x0800 + 0x00B4)
+#define REG_CLDMA_L3TISAR2 (0x0800 + 0x00C0)
+
+#define REG_CLDMA_L2RIMR0 (0x0800 + 0x00E8)
+#define REG_CLDMA_L2RIMR1 (0x0800 + 0x00EC)
+#define REG_CLDMA_L2RIMCR0 (0x0800 + 0x00F0)
+#define REG_CLDMA_L2RIMCR1 (0x0800 + 0x00F4)
+#define REG_CLDMA_L2RIMSR0 (0x0800 + 0x00F8)
+#define REG_CLDMA_L2RIMSR1 (0x0800 + 0x00FC)
+
+#define REG_CLDMA_INT_EAP_USIP_MASK (0x0800 + 0x011C)
+#define REG_CLDMA_INT_WF_MASK (0x0800 + 0x0120)
+#define REG_CLDMA_RQ1_GPD_DONE_CNT (0x0800 + 0x0174)
+#define REG_CLDMA_TQ1_GPD_DONE_CNT (0x0800 + 0x0184)
+
+#define REG_CLDMA_IP_BUSY_TO_PCIE_MASK (0x0800 + 0x0194)
+#define REG_CLDMA_IP_BUSY_TO_PCIE_MASK_SET (0x0800 + 0x0198)
+#define REG_CLDMA_IP_BUSY_TO_PCIE_MASK_CLR (0x0800 + 0x019C)
+
+#define REG_CLDMA_IP_BUSY_TO_AP_MASK (0x0800 + 0x0200)
+#define REG_CLDMA_IP_BUSY_TO_AP_MASK_SET (0x0800 + 0x0204)
+#define REG_CLDMA_IP_BUSY_TO_AP_MASK_CLR (0x0800 + 0x0208)
+#define REG_CLDMA_IP_BUSY_TO_MD_MASK_SET (0x0800 + 0x0210)
+#define REG_CLDMA_RX_WORK_TO_REG_MASK_SET (0x0800 + 0x021C)
+
+/* CLDMA RESET */
+#define REG_INFRA_RST0_SET (0x120)
+#define REG_INFRA_RST0_CLR (0x124)
+#define REG_CLDMA0_RST_SET_BIT (8)
+#define REG_CLDMA0_RST_CLR_BIT (8)
+
+#endif
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
new file mode 100644
index 000000000000..c1bb787ee981
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include "mtk_cldma.h"
+#include "mtk_trans_ctrl.h"
+
+#define TRB_SRV_NUM (1)
+
+static const int mtk_srv_cfg_m9xx[NR_CLDMA][HW_QUE_NUM] = {
+ {0},
+ {0},
+};
+
+static const struct queue_info mtk_queue_info_m9xx[] = {
+};
+
+struct mtk_ctrl_info mtk_ctrl_info_m9xx = {
+ .queue_info = (struct queue_info *)mtk_queue_info_m9xx,
+ .queue_info_num = ARRAY_SIZE(mtk_queue_info_m9xx),
+ .srv_cfg = (int **)mtk_srv_cfg_m9xx,
+ .trb_srv_num = TRB_SRV_NUM,
+};
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
index 9588635fab3d..da2a6926b801 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
@@ -877,6 +877,28 @@ static void mtk_pci_free_irq(struct mtk_md_dev *mdev)
pci_free_irq_vectors(pdev);
}
+static int mtk_pci_dev_init(struct mtk_md_dev *mdev)
+{
+ int ret;
+
+ ret = mtk_trans_ctrl_init(mdev);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to initialize control plane: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void mtk_pci_dev_exit(struct mtk_md_dev *mdev)
+{
+ mtk_trans_ctrl_exit(mdev);
+}
+
+static int mtk_pci_dev_start(struct mtk_md_dev *mdev)
+{
+ return 0;
+}
static const struct mtk_dev_ops pci_hw_ops = {
.get_dev_state = mtk_pci_get_dev_state,
.ack_dev_state = mtk_pci_ack_dev_state,
@@ -953,6 +975,12 @@ static int mtk_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
if (ret)
goto free_mhccif;
+ ret = mtk_pci_dev_init(mdev);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to init dev.\n");
+ goto free_irq;
+ }
+
pci_set_master(pdev);
mtk_pci_unmask_irq(mdev, priv->mhccif_irq_id);
@@ -969,10 +997,20 @@ static int mtk_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
goto clear_master;
}
+ ret = mtk_pci_dev_start(mdev);
+ if (ret) {
+ dev_err(mdev->dev, "Failed to start dev.\n");
+ goto free_saved_state;
+ }
+
return 0;
+free_saved_state:
+ pci_load_and_free_saved_state(pdev, &priv->saved_state);
clear_master:
pci_clear_master(pdev);
+ mtk_pci_dev_exit(mdev);
+free_irq:
mtk_pci_free_irq(mdev);
free_mhccif:
mtk_mhccif_exit(mdev);
@@ -996,6 +1034,7 @@ static void mtk_pci_remove(struct pci_dev *pdev)
}
pci_clear_master(pdev);
+ mtk_pci_dev_exit(mdev);
mtk_pci_free_irq(mdev);
mtk_mhccif_exit(mdev);
pci_load_and_free_saved_state(pdev, &priv->saved_state);
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h b/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h
index 3f0667e8a846..73299ae03f89 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci_reg.h
@@ -21,6 +21,7 @@
#define REG_IMASK_HOST_MSIX_SET_GRP0_0 0x3000
#define REG_IMASK_HOST_MSIX_CLR_GRP0_0 0x3080
#define REG_IMASK_HOST_MSIX_GRP0_0 0x3100
+#define REG_DEV_INFRA_BASE 0x10001000
/* mhccif registers */
#define MHCCIF_RC2EP_SW_BSY 0x4
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
new file mode 100644
index 000000000000..55297cfcdbc6
--- /dev/null
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
@@ -0,0 +1,541 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/device.h>
+#include <linux/freezer.h>
+#include <linux/hashtable.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/nospec.h>
+#include <linux/sched.h>
+#include <linux/wait.h>
+
+#include "mtk_cldma.h"
+#include "mtk_ctrl_plane.h"
+#include "mtk_dev.h"
+#include "mtk_pci.h"
+#include "mtk_trans_ctrl.h"
+
+#define MTK_DFLT_PORT_NAME_LEN (20)
+
+static struct mtk_ctrl_info_desc mtk_ctrl_info_tbl[] = {
+ {2304, &ctrl_info_name(m9xx)},
+ {0, NULL},
+};
+
+#define QUEUE_CHL_MASK 0xFFFF
+
+static bool mtk_queue_list_is_full(struct mtk_ctrl_trans *trans, struct queue_info *que)
+{
+ return trans->trans_list[que->hif_id].skb_list[que->txqno].qlen >= SKB_LIST_MAX_LEN;
+}
+
+static bool mtk_ctrl_chs_is_busy_or_empty(struct trb_srv *srv)
+{
+ struct srv_que *srv_que;
+ int i;
+
+ for (i = 0; i < NR_CLDMA; i++)
+ list_for_each_entry(srv_que, &srv->srv_q_list[i], list)
+ if (!skb_queue_empty(&srv->trans->trans_list[i].skb_list[srv_que->qno]) &&
+ mtk_cldma_get_tx_budget(srv->trans->dev, i, srv_que->qno))
+ return false;
+
+ return true;
+}
+
+static void mtk_ctrl_ch_flush(struct sk_buff_head *skb_list)
+{
+ struct sk_buff *skb;
+ struct trb *trb;
+
+ while (!skb_queue_empty(skb_list)) {
+ skb = skb_dequeue(skb_list);
+ trb = (struct trb *)skb->cb;
+ trb->status = -EIO;
+ trb->trb_complete(skb);
+ }
+}
+
+static void mtk_ctrl_chs_flush(struct trb_srv *srv)
+{
+ struct srv_que *srv_que;
+ int i;
+
+ for (i = 0; i < NR_CLDMA; i++)
+ list_for_each_entry(srv_que, &srv->srv_q_list[i], list)
+ mtk_ctrl_ch_flush(&srv->trans->trans_list[i].skb_list[srv_que->qno]);
+}
+
+static int mtk_ch_status_check(struct mtk_ctrl_trans *trans, struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct trb_open_priv *trb_open_priv;
+ struct queue_info *que;
+ int ret = 0;
+
+ que = radix_tree_lookup(&trans->queue_tbl, trb->channel_id & QUEUE_CHL_MASK);
+
+ switch (trb->cmd) {
+ case TRB_CMD_ENABLE:
+ trb_open_priv = (struct trb_open_priv *)skb->data;
+ trb_open_priv->log_rg_offset = que->log_rg_offset;
+ trans->usr_cnt[que->hif_id][que->txqno]++;
+ if (trans->usr_cnt[que->hif_id][que->txqno] == 1)
+ break;
+ trb_open_priv->tx_mtu = que->tx_mtu;
+ trb_open_priv->rx_mtu = que->rx_mtu;
+ trb_open_priv->tx_frag_size = que->tx_frag_size;
+ trb_open_priv->rx_frag_size = que->rx_frag_size;
+ if (mtk_cldma_check_ch_cfg(trans->dev, que)) {
+ trb->status = -EINVAL;
+ ret = -EINVAL;
+ } else {
+ trb->status = -EBUSY;
+ ret = -EBUSY;
+ }
+ trb->trb_complete(skb);
+ break;
+ case TRB_CMD_DISABLE:
+ if (trans->usr_cnt[que->hif_id][que->txqno] > 0) {
+ trans->usr_cnt[que->hif_id][que->txqno]--;
+ if (!trans->usr_cnt[que->hif_id][que->txqno])
+ break;
+ }
+ trb->status = -EBUSY;
+ trb->trb_complete(skb);
+ ret = -EBUSY;
+ break;
+ default:
+ dev_err((trans->mdev)->dev, "Invalid trb command(%d)\n", trb->cmd);
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
+static void mtk_ctrl_trb_handler(struct trb_srv *srv, struct trans_list *trans_list, u32 qno)
+{
+ struct sk_buff_head *skb_list = &trans_list->skb_list[qno];
+ struct mtk_ctrl_trans *trans = srv->trans;
+ struct sk_buff *skb, *skb_next;
+ struct trb *trb, *trb_next;
+ bool kick = false;
+ int loop = 0;
+ int err;
+
+ do {
+ skb = skb_peek(skb_list);
+ if (!skb)
+ break;
+ trb = (struct trb *)skb->cb;
+
+ switch (trb->cmd) {
+ case TRB_CMD_ENABLE:
+ case TRB_CMD_DISABLE:
+ skb_unlink(skb, skb_list);
+ err = mtk_ch_status_check(trans, skb);
+ if (!err) {
+ kick = true;
+ if (trb->cmd == TRB_CMD_DISABLE)
+ mtk_ctrl_ch_flush(skb_list);
+ }
+ break;
+ case TRB_CMD_TX:
+ err = mtk_cldma_submit_tx(trans->dev, skb);
+ if (err) {
+ if (trans_list->tx_burst_cnt[qno]) {
+ kick = true;
+ break;
+ }
+ if (err == -EAGAIN)
+ return;
+
+ skb_unlink(skb, skb_list);
+ trb->status = err;
+ trb->trb_complete(skb);
+ break;
+ }
+
+ trans_list->tx_burst_cnt[qno]++;
+ if (trans_list->tx_burst_cnt[qno] >= TX_BURST_MAX_CNT ||
+ skb_queue_is_last(skb_list, skb)) {
+ kick = true;
+ } else {
+ skb_next = skb_peek_next(skb, skb_list);
+ trb_next = (struct trb *)skb_next->cb;
+ if (trb_next->cmd != TRB_CMD_TX)
+ kick = true;
+ }
+
+ skb_unlink(skb, skb_list);
+ break;
+ default:
+ skb_unlink(skb, skb_list);
+ }
+
+ if (kick) {
+ mtk_cldma_trb_process(trans->dev, skb);
+ trans_list->tx_burst_cnt[qno] = 0;
+ kick = false;
+ }
+
+ loop++;
+ } while (loop < TRB_NUM_PER_ROUND);
+}
+
+static void mtk_ctrl_trb_process(struct trb_srv *srv)
+{
+ struct mtk_ctrl_trans *trans = srv->trans;
+ struct srv_que *srv_que;
+ int i;
+
+ for (i = 0; i < NR_CLDMA; i++)
+ list_for_each_entry(srv_que, &srv->srv_q_list[i], list)
+ mtk_ctrl_trb_handler(srv, &trans->trans_list[i], srv_que->qno);
+}
+
+static int mtk_ctrl_trb_thread(void *args)
+{
+ struct trb_srv *srv = args;
+
+ for (;;) {
+ wait_event_interruptible(srv->trb_waitq,
+ !mtk_ctrl_chs_is_busy_or_empty(srv) ||
+ kthread_should_stop() || kthread_should_park());
+ if (kthread_should_stop())
+ break;
+
+ if (kthread_should_park())
+ kthread_parkme();
+
+ do {
+ mtk_ctrl_trb_process(srv);
+ cond_resched();
+ } while (!mtk_ctrl_chs_is_busy_or_empty(srv) && !kthread_should_stop() &&
+ !kthread_should_park());
+ }
+ mtk_ctrl_chs_flush(srv);
+ return 0;
+}
+
+static int mtk_ctrl_trb_srv_init(struct mtk_ctrl_trans *trans)
+{
+ struct srv_que *srv_que;
+ struct trb_srv *srv;
+ int i, j;
+ int ret;
+
+ for (i = 0; i < trans->trb_srv_num; i++) {
+ srv = kzalloc_obj(*srv);
+ if (!srv) {
+ ret = -ENOMEM;
+ goto err_free_srv;
+ }
+
+ srv->trans = trans;
+ srv->srv_id = i;
+ trans->trb_srv[i] = srv;
+
+ init_waitqueue_head(&srv->trb_waitq);
+ for (j = 0; j < NR_CLDMA; j++)
+ INIT_LIST_HEAD(&srv->srv_q_list[j]);
+ }
+
+ for (i = 0; i < NR_CLDMA; i++)
+ for (j = 0; j < HW_QUE_NUM; j++) {
+ if (trans->srv_cfg[i][j] < 0 ||
+ trans->srv_cfg[i][j] >= trans->trb_srv_num)
+ trans->srv_cfg[i][j] = 0;
+ srv_que = kzalloc_obj(*srv_que);
+ if (!srv_que) {
+ ret = -ENOMEM;
+ goto err_free_srv_que;
+ }
+ srv_que->hif_id = i;
+ srv_que->qno = j;
+ list_add_tail(&srv_que->list,
+ &trans->trb_srv[trans->srv_cfg[i][j]]->srv_q_list[i]);
+ }
+
+ for (i = 0; i < trans->trb_srv_num; i++) {
+ trans->trb_srv[i]->trb_thread = kthread_run(mtk_ctrl_trb_thread, trans->trb_srv[i],
+ "mtk_trb_srv%d_%s", i,
+ trans->mdev->dev_str);
+ if (IS_ERR(trans->trb_srv[i]->trb_thread)) {
+ ret = PTR_ERR(trans->trb_srv[i]->trb_thread);
+ trans->trb_srv[i]->trb_thread = NULL;
+ goto err_stop_kthread;
+ }
+ }
+
+ return 0;
+err_stop_kthread:
+ while (--i >= 0)
+ kthread_stop(trans->trb_srv[i]->trb_thread);
+err_free_srv_que:
+ for (i = 0; i < trans->trb_srv_num; i++) {
+ for (j = 0; j < NR_CLDMA; j++) {
+ struct srv_que *next_srv_que;
+
+ list_for_each_entry_safe(srv_que, next_srv_que,
+ &trans->trb_srv[i]->srv_q_list[j], list) {
+ list_del(&srv_que->list);
+ kfree(srv_que);
+ }
+ }
+ }
+err_free_srv:
+ for (i = 0; i < trans->trb_srv_num; i++) {
+ if (!trans->trb_srv[i])
+ break;
+ kfree(trans->trb_srv[i]);
+ trans->trb_srv[i] = NULL;
+ }
+
+ return ret;
+}
+
+static void mtk_ctrl_trb_srv_exit(struct mtk_ctrl_trans *trans)
+{
+ struct srv_que *srv_que, *next_srv_que;
+ struct trb_srv *srv;
+ int i, j;
+
+ for (i = 0; i < trans->trb_srv_num; i++) {
+ srv = trans->trb_srv[i];
+ kthread_stop(srv->trb_thread);
+ for (j = 0; j < NR_CLDMA; j++) {
+ list_for_each_entry_safe(srv_que, next_srv_que,
+ &trans->trb_srv[i]->srv_q_list[j], list) {
+ list_del(&srv_que->list);
+ kfree(srv_que);
+ }
+ }
+ kfree(srv);
+ trans->trb_srv[i] = NULL;
+ }
+}
+
+static void mtk_ctrl_remove_radix_tree(struct mtk_ctrl_trans *trans)
+{
+ struct radix_tree_iter iter;
+ struct queue_info *queue;
+ void __rcu **slot;
+
+ radix_tree_for_each_slot(slot, &trans->queue_tbl, &iter, 0) {
+ queue = radix_tree_deref_slot(slot);
+ if (!queue)
+ continue;
+ radix_tree_delete(&trans->queue_tbl, iter.index);
+ kfree(queue);
+ }
+}
+
+static int mtk_pcie_hif_init(struct mtk_md_dev *mdev)
+{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ struct queue_info *queue, *queue_info;
+ struct mtk_ctrl_trans *trans;
+ int i, j;
+ int ret;
+
+ trans = ctrl_blk->ctrl_hw_priv;
+ trans->ctrl_blk = ctrl_blk;
+ queue_info = trans->queue_info;
+
+ INIT_RADIX_TREE(&trans->queue_tbl, GFP_KERNEL);
+ for (i = 0; i < trans->queue_info_num; i++) {
+ queue = kmemdup(queue_info + i, sizeof(*queue), GFP_KERNEL);
+ if (!queue) {
+ ret = -ENOMEM;
+ goto err_free_radix_tree;
+ }
+ if (queue->txqno >= HW_QUE_NUM || queue->rxqno >= HW_QUE_NUM ||
+ queue->hif_id >= NR_CLDMA) {
+ dev_err(mdev->dev, "Failed to get correct queue info %x\n",
+ queue->rx_chl);
+ kfree(queue);
+ ret = -EINVAL;
+ goto err_free_radix_tree;
+ }
+ ret = radix_tree_insert(&trans->queue_tbl, queue->rx_chl & QUEUE_CHL_MASK, queue);
+ if (ret) {
+ dev_err(mdev->dev, "Insert %x fail, ret: %d", queue->rx_chl, ret);
+ kfree(queue);
+ goto err_free_radix_tree;
+ }
+ trans->queues_cnt++;
+ }
+
+ for (i = 0; i < NR_CLDMA; i++) {
+ for (j = 0; j < HW_QUE_NUM; j++) {
+ skb_queue_head_init(&trans->trans_list[i].skb_list[j]);
+ trans->trans_list[i].tx_burst_cnt[j] = 0;
+ }
+ }
+ ret = mtk_cldma_init(trans);
+ if (ret)
+ goto err_free_radix_tree;
+
+ ret = mtk_ctrl_trb_srv_init(trans);
+ if (ret)
+ goto err_cldma_exit;
+
+ atomic_set(&trans->available, 1);
+
+ return 0;
+
+err_cldma_exit:
+ mtk_cldma_exit(trans);
+err_free_radix_tree:
+ mtk_ctrl_remove_radix_tree(trans);
+
+ return ret;
+}
+
+static int mtk_pcie_hif_exit(struct mtk_md_dev *mdev)
+{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ struct mtk_ctrl_trans *trans;
+
+ trans = ctrl_blk->ctrl_hw_priv;
+
+ atomic_set(&trans->available, 0);
+ mtk_ctrl_trb_srv_exit(trans);
+ mtk_ctrl_remove_radix_tree(trans);
+ mtk_cldma_exit(trans);
+
+ return 0;
+}
+
+static int mtk_pcie_hif_submit_skb(struct mtk_md_dev *mdev, struct sk_buff *skb, bool force_send)
+{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ struct mtk_ctrl_trans *trans;
+ struct queue_info *que;
+ struct trb *trb;
+
+ trans = ctrl_blk->ctrl_hw_priv;
+ trb = (struct trb *)skb->cb;
+
+ if (trb->cmd == TRB_CMD_STOP || trb->cmd == TRB_CMD_RECOVER) {
+ trb->trb_complete(skb);
+ return 0;
+ }
+
+ que = radix_tree_lookup(&trans->queue_tbl, trb->channel_id & QUEUE_CHL_MASK);
+ if (!que) {
+ dev_warn(mdev->dev, "lookup que fail, ch_id: %x, que: 0x%p\n",
+ trb->channel_id, que);
+ return -EINVAL;
+ }
+
+ if (!atomic_read(&trans->available))
+ return -EIO;
+
+ if (mtk_queue_list_is_full(trans, que) && !force_send)
+ return -EAGAIN;
+
+ if (trb->cmd == TRB_CMD_DISABLE)
+ skb_queue_head(&trans->trans_list[que->hif_id].skb_list[que->txqno], skb);
+ else
+ skb_queue_tail(&trans->trans_list[que->hif_id].skb_list[que->txqno], skb);
+
+ wake_up(&trans->trb_srv[trans->srv_cfg[que->hif_id][que->txqno]]->trb_waitq);
+
+ return 0;
+}
+
+static int mtk_pcie_hif_cmd_func(struct mtk_md_dev *mdev, int cmd, void *data)
+{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ struct mtk_ctrl_trans *trans;
+ struct queue_info *que;
+
+ switch (cmd) {
+ case HIF_CTRL_CMD_CHECK_TX_FULL:
+ trans = ctrl_blk->ctrl_hw_priv;
+ que = radix_tree_lookup(&trans->queue_tbl,
+ ((union ctrl_hif_cmd_data *)data)->rx_ch & QUEUE_CHL_MASK);
+ if (!que) {
+ dev_warn(mdev->dev, "Failed to find que to check tx full\n");
+ return -EINVAL;
+ }
+ return mtk_queue_list_is_full(trans, que);
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static struct mtk_ctrl_hif_ops pcie_ctrl_ops = {
+ .init = mtk_pcie_hif_init,
+ .exit = mtk_pcie_hif_exit,
+ .submit_skb = mtk_pcie_hif_submit_skb,
+ .send_cmd = mtk_pcie_hif_cmd_func,
+};
+
+static void mtk_trans_get_ctrl_info(struct mtk_ctrl_cfg *cfg,
+ struct mtk_ctrl_trans *trans, u32 hw_ver)
+{
+ struct mtk_ctrl_info_desc *ctrl_info_desc;
+ struct mtk_ctrl_info *ctrl_info;
+ u8 i;
+
+ for (i = 0; (ctrl_info_desc = &mtk_ctrl_info_tbl[i]) && ctrl_info_desc &&
+ ctrl_info_desc->ctrl_info; i++) {
+ if (ctrl_info_desc->hw_ver != hw_ver)
+ continue;
+
+ ctrl_info = ctrl_info_desc->ctrl_info;
+ memcpy(trans->srv_cfg, ctrl_info->srv_cfg,
+ sizeof(int) * NR_CLDMA * HW_QUE_NUM);
+ trans->queue_info = ctrl_info->queue_info;
+ trans->queue_info_num = ctrl_info->queue_info_num;
+ trans->trb_srv_num = ctrl_info->trb_srv_num;
+ }
+}
+
+int mtk_trans_ctrl_init(struct mtk_md_dev *mdev)
+{
+ struct mtk_ctrl_trans *trans;
+ struct mtk_ctrl_blk *ctrl_blk;
+ int err;
+
+ trans = devm_kzalloc(mdev->dev, sizeof(*trans), GFP_KERNEL);
+ if (!trans)
+ return -ENOMEM;
+ trans->mdev = mdev;
+ trans->queues_cnt = 0;
+
+ mtk_trans_get_ctrl_info(NULL, trans, mdev->hw_ver);
+ if (!trans->queue_info ||
+ trans->trb_srv_num <= 0 || trans->trb_srv_num > TRB_SRV_MAX_NUM ||
+ trans->queue_info_num <= 0) {
+ dev_err(mdev->dev, "Failed to get ctrl info!\n");
+ goto err_free_cfg;
+ }
+
+ err = mtk_ctrl_init(mdev, &pcie_ctrl_ops);
+ if (err)
+ goto err_free_cfg;
+
+ ctrl_blk = mdev->ctrl_blk;
+ ctrl_blk->ctrl_hw_priv = trans;
+
+ return 0;
+
+err_free_cfg:
+ return -ENOMEM;
+}
+
+int mtk_trans_ctrl_exit(struct mtk_md_dev *mdev)
+{
+ mtk_ctrl_exit(mdev);
+
+ return 0;
+}
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
index d6de4c43b529..0d25d1f51671 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
@@ -13,9 +13,95 @@
#include "mtk_dev.h"
+#define TRB_SRV_MAX_NUM (1)
+#define HW_QUE_NUM (8)
+#define TX_GPD_NUM (16)
+#define RX_GPD_NUM (TX_GPD_NUM)
+#define MIN_GPD_NUM (2)
+#define SKB_LIST_MAX_LEN (16)
+#define MTU_RSV_ROOM (0x100)
+#define TRB_NUM_PER_ROUND (TX_GPD_NUM)
+#define TX_BURST_MAX_CNT (TX_GPD_NUM / 4 + 1)
+
+#define HIF_ID(peer_id) ((peer_id) - 1)
+
+enum mtk_hif_id {
+ CLDMA0,
+ CLDMA1,
+ CLDMA4,
+ NR_CLDMA
+};
+
+struct queue_info {
+ u32 tx_chl;
+ u32 rx_chl;
+ enum mtk_hif_id hif_id;
+ u32 txqno;
+ u32 rxqno;
+ u32 tx_mtu;
+ u32 rx_mtu;
+ u32 tx_nr_gpds;
+ u32 rx_nr_gpds;
+ u32 tx_frag_size;
+ u32 rx_frag_size;
+ u8 log_rg_offset;
+};
+
+struct trans_list {
+ struct sk_buff_head skb_list[HW_QUE_NUM];
+ u8 tx_burst_cnt[HW_QUE_NUM];
+};
+
struct mtk_ctrl_trans {
struct mtk_ctrl_blk *ctrl_blk;
+ struct trb_srv *trb_srv[TRB_SRV_MAX_NUM];
+ struct trans_list trans_list[NR_CLDMA];
+ void *dev;
+ struct radix_tree_root queue_tbl;
struct mtk_md_dev *mdev;
+ int usr_cnt[NR_CLDMA][HW_QUE_NUM];
+ u32 tx_mtu_cfg[NR_CLDMA][HW_QUE_NUM];
+ u32 rx_mtu_cfg[NR_CLDMA][HW_QUE_NUM];
+ atomic_t available;
+ int queues_cnt;
+ int srv_cfg[NR_CLDMA][HW_QUE_NUM];
+ struct queue_info *queue_info;
+ int queue_info_num;
+ int trb_srv_num;
+};
+
+struct srv_que {
+ u32 hif_id;
+ u32 qno;
+ struct list_head list;
};
+struct trb_srv {
+ u32 srv_id;
+ struct list_head srv_q_list[NR_CLDMA];
+ struct mtk_ctrl_trans *trans;
+ wait_queue_head_t trb_waitq;
+ struct task_struct *trb_thread;
+};
+
+struct mtk_ctrl_info {
+ struct mtk_ctrl_cfg *ctrl_cfg;
+ int **srv_cfg;
+ struct queue_info *queue_info;
+ u32 queue_info_num;
+ u32 trb_srv_num;
+};
+
+struct mtk_ctrl_info_desc {
+ u32 hw_ver;
+ struct mtk_ctrl_info *ctrl_info;
+};
+
+#define ctrl_info_name(NAME) mtk_ctrl_info_##NAME
+
+extern struct mtk_ctrl_info mtk_ctrl_info_m9xx;
+
+int mtk_trans_ctrl_init(struct mtk_md_dev *mdev);
+int mtk_trans_ctrl_exit(struct mtk_md_dev *mdev);
+
#endif
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 4/7] net: wwan: t9xx: Add control port
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
` (2 preceding siblings ...)
2026-08-11 7:14 ` [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
` (3 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
The control port consists of port I/O and port manager.
Port I/O provides a common operation as defined by "struct port_ops",
and the operation is managed by the "port manager". It provides
interfaces to internal users, the implemented internal interfaces are
open, close, write and recv_register.
The port manager defines and implements port management interfaces and
structures. It is responsible for port creation, destroying, and managing
port states. It sends data from port I/O to CLDMA via TRB ( Transaction
Request Block ), and dispatches received data from CLDMA to port I/O.
The using port will be held in the "stale list" when the driver destroys
it, and after creating it again, the user can continue to use it.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/t9xx/Makefile | 4 +-
drivers/net/wwan/t9xx/mtk_ctrl_plane.c | 19 +-
drivers/net/wwan/t9xx/mtk_ctrl_plane.h | 20 +-
drivers/net/wwan/t9xx/mtk_dev.c | 22 +
drivers/net/wwan/t9xx/mtk_port.c | 875 +++++++++++++++++++++++++
drivers/net/wwan/t9xx/mtk_port.h | 159 +++++
drivers/net/wwan/t9xx/mtk_port_io.c | 238 +++++++
drivers/net/wwan/t9xx/mtk_port_io.h | 36 +
drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c | 25 +-
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 2 +
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c | 22 +-
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h | 1 +
12 files changed, 1412 insertions(+), 11 deletions(-)
diff --git a/drivers/net/wwan/t9xx/Makefile b/drivers/net/wwan/t9xx/Makefile
index ae9d6f2344ab..db3b1aa1928b 100644
--- a/drivers/net/wwan/t9xx/Makefile
+++ b/drivers/net/wwan/t9xx/Makefile
@@ -8,4 +8,6 @@ obj-$(CONFIG_MTK_T9XX_PCI) += pcie/
mtk_t9xx-y := \
mtk_dev.o \
- mtk_ctrl_plane.o
+ mtk_ctrl_plane.o \
+ mtk_port.o \
+ mtk_port_io.o
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
index 4be699d1ec00..6ed47d074556 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
@@ -7,20 +7,23 @@
#include <linux/device.h>
#include "mtk_ctrl_plane.h"
+#include "mtk_port.h"
/**
* mtk_ctrl_init() - Initialize the control plane block.
* @mdev: Pointer to the MTK modem device.
* @ops: HIF operations for the control plane.
+ * @cfg: Control plane configuration.
*
* Allocates and initializes the control plane block
* associated with @mdev.
*
- * Return: 0 on success, -ENOMEM on allocation failure.
+ * Return: 0 on success, negative error code on failure.
*/
-int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops)
+int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops, struct mtk_ctrl_cfg *cfg)
{
struct mtk_ctrl_blk *ctrl_blk;
+ int err;
ctrl_blk = devm_kzalloc(mdev->dev, sizeof(*ctrl_blk), GFP_KERNEL);
if (!ctrl_blk)
@@ -29,8 +32,17 @@ int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops)
ctrl_blk->mdev = mdev;
mdev->ctrl_blk = ctrl_blk;
ctrl_blk->ops = ops;
+ ctrl_blk->cfg = cfg;
+
+ err = mtk_port_mngr_init(ctrl_blk, cfg->port_layer_cfg->port_cfg,
+ cfg->port_layer_cfg->port_cnt);
+ if (err)
+ goto err_free_mem;
return 0;
+
+err_free_mem:
+ return err;
}
EXPORT_SYMBOL(mtk_ctrl_init);
@@ -42,6 +54,9 @@ EXPORT_SYMBOL(mtk_ctrl_init);
*/
void mtk_ctrl_exit(struct mtk_md_dev *mdev)
{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+
+ mtk_port_mngr_exit(ctrl_blk);
mdev->ctrl_blk = NULL;
}
EXPORT_SYMBOL(mtk_ctrl_exit);
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
index 88d71ac92084..d7fcccde8a1b 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
@@ -11,6 +11,17 @@
#include "mtk_dev.h"
+#define Q_MTU_2K (0x800)
+#define Q_MTU_3_5K (0xE00)
+#define Q_MTU_7K (0x1C00)
+#define Q_MTU_32K (0x8000)
+#define Q_MTU_63K (0xFC00)
+#define Q_FRAG_2K (0x800)
+#define Q_FRAG_3_5K (0xE00)
+#define Q_FRAG_7K (0x1C00)
+#define Q_FRAG_32K (0x8000)
+#define Q_FRAG_63K (0xFC00)
+
enum mtk_trb_cmd_type {
TRB_CMD_MIN,
TRB_CMD_ENABLE,
@@ -54,17 +65,22 @@ struct mtk_ctrl_hif_ops {
int (*send_cmd)(struct mtk_md_dev *mdev, int cmd, void *data);
};
-struct mtk_ctrl_cfg;
+struct mtk_ctrl_cfg {
+ struct mtk_port_layer_cfg *port_layer_cfg;
+};
+
struct mtk_ctrl_trans;
struct mtk_ctrl_blk {
struct mtk_md_dev *mdev;
+ struct mtk_port_mngr *port_mngr;
struct mtk_ctrl_hif_ops *ops;
void *ctrl_hw_priv;
struct mtk_ctrl_cfg *cfg;
};
-int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops);
+int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops,
+ struct mtk_ctrl_cfg *cfg);
void mtk_ctrl_exit(struct mtk_md_dev *mdev);
#endif /* __MTK_CTRL_PLANE_H__ */
diff --git a/drivers/net/wwan/t9xx/mtk_dev.c b/drivers/net/wwan/t9xx/mtk_dev.c
index 50f77d5a210c..7c449030a35f 100644
--- a/drivers/net/wwan/t9xx/mtk_dev.c
+++ b/drivers/net/wwan/t9xx/mtk_dev.c
@@ -6,6 +6,28 @@
#include <linux/module.h>
#include "mtk_dev.h"
+#include "mtk_port.h"
+#include "mtk_port_io.h"
+
+static int __init mtk_common_drv_init(void)
+{
+ int ret;
+
+ ret = mtk_port_io_init();
+ if (ret)
+ goto err_init_devid;
+
+err_init_devid:
+ return ret;
+}
+module_init(mtk_common_drv_init);
+
+static void __exit mtk_common_drv_exit(void)
+{
+ mtk_port_io_exit();
+ mtk_port_stale_list_grp_cleanup();
+}
+module_exit(mtk_common_drv_exit);
MODULE_DESCRIPTION("MediaTek T9xx PCIe WWAN driver");
MODULE_LICENSE("GPL");
diff --git a/drivers/net/wwan/t9xx/mtk_port.c b/drivers/net/wwan/t9xx/mtk_port.c
new file mode 100644
index 000000000000..a71c097c6d39
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_port.c
@@ -0,0 +1,875 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/netdevice.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+
+#include "mtk_port.h"
+#include "mtk_port_io.h"
+
+#define MTK_DFLT_TRB_TIMEOUT (5 * HZ)
+#define MTK_DFLT_TRB_STATUS (0x1)
+#define MTK_TRB_HEADER_ADDED (0xADDED)
+#define MTK_CHECK_RX_SEQ_MASK (0x7fff)
+
+#define MTK_PORT_ENUM_VER (0)
+#define MTK_PORT_ENUM_HEAD_PATTERN (0x5a5a5a5a)
+#define MTK_PORT_ENUM_TAIL_PATTERN (0xa5a5a5a5)
+
+#define MTK_PORT_SEARCH_FROM_RADIX_TREE(p, s) ({\
+ struct mtk_port *_p; \
+ _p = rcu_dereference_raw(*(s)); \
+ if (!_p) \
+ continue; \
+ p = _p; \
+})
+
+#define MTK_PORT_INTERNAL_NODE_CHECK(p, s, i) ({\
+ if (radix_tree_is_internal_node(p)) { \
+ s = radix_tree_iter_retry(&(i));\
+ continue; \
+ } \
+})
+
+struct mtk_port_info {
+ __le16 channel;
+ __le16 reserved;
+} __packed;
+
+struct mtk_port_enum_msg {
+ __le32 head_pattern;
+ __le16 port_cnt;
+ __le16 version;
+ __le32 tail_pattern;
+ u8 data[];
+} __packed;
+
+/* global group for stale ports */
+static LIST_HEAD(stale_list_grp);
+/* mutex lock for stale_list_group */
+DEFINE_MUTEX(port_mngr_grp_mtx);
+
+static DEFINE_IDA(ccci_dev_ids);
+
+/* This function working always under mutex lock port_mngr_grp_mtx */
+void mtk_port_release(struct kref *port_kref)
+{
+ struct mtk_stale_list *s_list;
+ struct mtk_port *port;
+
+ port = container_of(port_kref, struct mtk_port, kref);
+ if (!test_bit(PORT_S_ON_STALE_LIST, &port->status))
+ goto port_exit;
+
+ list_del(&port->stale_entry);
+ list_for_each_entry(s_list, &stale_list_grp, entry) {
+ if (!strncmp(s_list->dev_str, port->dev_str, MTK_DEV_STR_LEN) &&
+ list_empty(&s_list->ports) && s_list->dev_id >= 0) {
+ ida_free(&ccci_dev_ids, s_list->dev_id);
+ s_list->dev_id = -1;
+ break;
+ }
+ }
+port_exit:
+ ports_ops[port->info.type]->exit(port);
+ kfree(port);
+}
+
+static int mtk_port_tbl_add(struct mtk_port_mngr *port_mngr, struct mtk_port *port)
+{
+ int ret;
+
+ ret = radix_tree_insert(&port_mngr->port_tbl[MTK_PORT_TBL_TYPE(port->info.rx_ch)],
+ port->info.rx_ch & 0xFFF, port);
+ if (ret)
+ dev_err(port_mngr->ctrl_blk->mdev->dev,
+ "port(%s) add to port_tbl failed, return %d\n",
+ port->info.name, ret);
+ else
+ port_mngr->port_cnt++;
+
+ return ret;
+}
+
+static void mtk_port_tbl_del(struct mtk_port_mngr *port_mngr, struct mtk_port *port)
+{
+ radix_tree_delete(&port_mngr->port_tbl[MTK_PORT_TBL_TYPE(port->info.rx_ch)],
+ port->info.rx_ch & 0xFFF);
+ port_mngr->port_cnt--;
+}
+
+static struct mtk_port *mtk_port_restore_from_stale_list(struct mtk_port_mngr *port_mngr,
+ struct mtk_stale_list *s_list)
+{
+ struct mtk_port *port, *next_port;
+ int ret;
+
+ mutex_lock(&port_mngr_grp_mtx);
+ list_for_each_entry_safe(port, next_port, &s_list->ports, stale_entry) {
+ kref_get(&port->kref);
+ list_del(&port->stale_entry);
+ ret = mtk_port_tbl_add(port_mngr, port);
+ if (ret) {
+ list_add_tail(&port->stale_entry, &s_list->ports);
+ kref_put(&port->kref, mtk_port_release);
+ mutex_unlock(&port_mngr_grp_mtx);
+ dev_err(port_mngr->ctrl_blk->mdev->dev,
+ "Failed when adding (%s) to port mngr\n",
+ port->info.name);
+ return ERR_PTR(ret);
+ }
+
+ port->port_mngr = port_mngr;
+ clear_bit(PORT_S_ON_STALE_LIST, &port->status);
+ ports_ops[port->info.type]->reset(port);
+ }
+ mutex_unlock(&port_mngr_grp_mtx);
+
+ return NULL;
+}
+
+static struct mtk_port *mtk_port_alloc_and_add(struct mtk_port_mngr *port_mngr,
+ struct mtk_port_cfg *dflt_info)
+{
+ struct mtk_port *port;
+ int ret;
+
+ port = kzalloc_obj(*port, GFP_KERNEL);
+ if (!port) {
+ ret = -ENOMEM;
+ goto err_alloc_port;
+ }
+ memcpy(&port->info, dflt_info, sizeof(*dflt_info));
+
+ ret = mtk_port_tbl_add(port_mngr, port);
+ if (ret < 0) {
+ dev_err(port_mngr->ctrl_blk->mdev->dev,
+ "Failed to add port(%s) to port tbl\n", dflt_info->name);
+ goto err_free_port;
+ }
+
+ port->port_mngr = port_mngr;
+ ret = ports_ops[port->info.type]->init(port);
+ if (ret < 0) {
+ mtk_port_tbl_del(port_mngr, port);
+ goto err_free_port;
+ }
+
+ memcpy(port->dev_str, port_mngr->ctrl_blk->mdev->dev_str, MTK_DEV_STR_LEN);
+ return port;
+
+err_free_port:
+ kfree(port);
+err_alloc_port:
+ return ERR_PTR(ret);
+}
+
+static void mtk_port_free_or_backup(struct mtk_port_mngr *port_mngr,
+ struct mtk_port *port, struct mtk_stale_list *s_list)
+{
+ mutex_lock(&port_mngr_grp_mtx);
+ mtk_port_tbl_del(port_mngr, port);
+ if (port->info.type != PORT_TYPE_INTERNAL) {
+ if (test_bit(PORT_S_OPEN, &port->status)) {
+ list_add_tail(&port->stale_entry, &s_list->ports);
+ set_bit(PORT_S_ON_STALE_LIST, &port->status);
+ memcpy(port->dev_str, port_mngr->ctrl_blk->mdev->dev_str,
+ MTK_DEV_STR_LEN);
+ port->port_mngr = NULL;
+ }
+ kref_put(&port->kref, mtk_port_release);
+ } else {
+ kref_put(&port->kref, mtk_port_release);
+ }
+ mutex_unlock(&port_mngr_grp_mtx);
+}
+
+static struct mtk_port *mtk_port_search_by_id(struct mtk_port_mngr *port_mngr, int rx_ch)
+{
+ int tbl_type = MTK_PORT_TBL_TYPE(rx_ch);
+
+ if (tbl_type < PORT_TBL_SAP || tbl_type >= PORT_TBL_MAX)
+ return NULL;
+
+ return radix_tree_lookup(&port_mngr->port_tbl[tbl_type], MTK_CH_ID(rx_ch));
+}
+
+struct mtk_port *mtk_port_search_by_name(struct mtk_port_mngr *port_mngr, char *name)
+{
+ int tbl_type = PORT_TBL_SAP;
+ struct radix_tree_iter iter;
+ struct mtk_port *port;
+ void __rcu **slot;
+
+ do {
+ radix_tree_for_each_slot(slot, &port_mngr->port_tbl[tbl_type], &iter, 0) {
+ MTK_PORT_SEARCH_FROM_RADIX_TREE(port, slot);
+ MTK_PORT_INTERNAL_NODE_CHECK(port, slot, iter);
+ if (!strncmp(port->info.name, name, MTK_DFLT_PORT_NAME_LEN))
+ return port;
+ }
+ tbl_type++;
+ } while (tbl_type < PORT_TBL_MAX);
+
+ return NULL;
+}
+
+static int mtk_port_tbl_create(struct mtk_port_mngr *port_mngr, struct mtk_port_cfg *cfg,
+ const int port_cnt, struct mtk_stale_list *s_list)
+{
+ struct mtk_port_cfg *dflt_port;
+ struct mtk_port *port;
+ int i;
+
+ INIT_RADIX_TREE(&port_mngr->port_tbl[PORT_TBL_SAP], GFP_KERNEL);
+ INIT_RADIX_TREE(&port_mngr->port_tbl[PORT_TBL_MD], GFP_KERNEL);
+
+ mtk_port_restore_from_stale_list(port_mngr, s_list);
+
+ /* copy ports from static port cfg table */
+ for (i = 0; i < port_cnt; i++) {
+ dflt_port = cfg + i;
+ if (!mtk_port_search_by_id(port_mngr, dflt_port->rx_ch)) {
+ port = mtk_port_alloc_and_add(port_mngr, dflt_port);
+ if (IS_ERR(port))
+ return PTR_ERR(port);
+ }
+ }
+
+ return 0;
+}
+
+static void mtk_port_tbl_destroy(struct mtk_port_mngr *port_mngr, struct mtk_stale_list *s_list)
+{
+ struct radix_tree_iter iter;
+ struct mtk_port *port;
+ void __rcu **slot;
+ int tbl_type;
+
+ tbl_type = PORT_TBL_SAP;
+ do {
+ radix_tree_for_each_slot(slot, &port_mngr->port_tbl[tbl_type], &iter, 0) {
+ port = radix_tree_deref_slot(slot);
+ if (!port)
+ continue;
+ ports_ops[port->info.type]->disable(port);
+ }
+
+ while (radix_tree_gang_lookup(&port_mngr->port_tbl[tbl_type],
+ (void **)&port, 0, 1))
+ mtk_port_free_or_backup(port_mngr, port, s_list);
+ } while (++tbl_type < PORT_TBL_MAX);
+}
+
+static struct mtk_stale_list *mtk_port_stale_list_create(struct mtk_ctrl_blk *ctrl_blk)
+{
+ struct mtk_stale_list *s_list;
+
+ s_list = kzalloc_obj(*s_list, GFP_KERNEL);
+ if (!s_list)
+ return NULL;
+
+ memcpy(s_list->dev_str, ctrl_blk->mdev->dev_str, MTK_DEV_STR_LEN);
+ s_list->dev_id = -1;
+ INIT_LIST_HEAD(&s_list->ports);
+ rwlock_init(&s_list->port_mngr_lock);
+
+ mutex_lock(&port_mngr_grp_mtx);
+ list_add_tail(&s_list->entry, &stale_list_grp);
+ mutex_unlock(&port_mngr_grp_mtx);
+
+ return s_list;
+}
+
+static void mtk_port_stale_list_destroy(struct mtk_stale_list *s_list)
+{
+ mutex_lock(&port_mngr_grp_mtx);
+ list_del(&s_list->entry);
+ mutex_unlock(&port_mngr_grp_mtx);
+ kfree(s_list);
+}
+
+static struct mtk_stale_list *mtk_port_stale_list_search(const char *dev_str)
+{
+ struct mtk_stale_list *tmp, *s_list = NULL;
+
+ mutex_lock(&port_mngr_grp_mtx);
+ list_for_each_entry(tmp, &stale_list_grp, entry) {
+ if (!strncmp(tmp->dev_str, dev_str, MTK_DEV_STR_LEN)) {
+ s_list = tmp;
+ break;
+ }
+ }
+ mutex_unlock(&port_mngr_grp_mtx);
+
+ return s_list;
+}
+
+void mtk_port_stale_list_grp_cleanup(void)
+{
+ struct mtk_stale_list *s_list, *next_s_list;
+ struct mtk_port *port, *next_port;
+
+ mutex_lock(&port_mngr_grp_mtx);
+ list_for_each_entry_safe(s_list, next_s_list, &stale_list_grp, entry) {
+ list_del(&s_list->entry);
+
+ list_for_each_entry_safe(port, next_port, &s_list->ports, stale_entry) {
+ clear_bit(PORT_S_ON_STALE_LIST, &port->status);
+ kref_put(&port->kref, mtk_port_release);
+ }
+
+ if (s_list->dev_id >= 0)
+ ida_free(&ccci_dev_ids, s_list->dev_id);
+ kfree(s_list);
+ }
+ mutex_unlock(&port_mngr_grp_mtx);
+}
+
+static struct mtk_stale_list *mtk_port_stale_list_init(struct mtk_ctrl_blk *ctrl_blk, int *dev_id)
+{
+ struct mtk_stale_list *s_list;
+
+ s_list = mtk_port_stale_list_search(ctrl_blk->mdev->dev_str);
+ if (!s_list) {
+ s_list = mtk_port_stale_list_create(ctrl_blk);
+ if (unlikely(!s_list))
+ return NULL;
+ }
+
+ mutex_lock(&port_mngr_grp_mtx);
+ if (s_list->dev_id < 0) {
+ *dev_id = ida_alloc_range(&ccci_dev_ids, 0, MTK_DFLT_MAX_DEV_CNT - 1, GFP_KERNEL);
+ } else {
+ *dev_id = s_list->dev_id;
+ s_list->dev_id = -1;
+ }
+ mutex_unlock(&port_mngr_grp_mtx);
+
+ return s_list;
+}
+
+static void mtk_port_stale_list_exit(struct mtk_ctrl_blk *ctrl_blk,
+ struct mtk_stale_list *s_list, int dev_id)
+{
+ if (!s_list)
+ return;
+ mutex_lock(&port_mngr_grp_mtx);
+ if (list_empty(&s_list->ports)) {
+ ida_free(&ccci_dev_ids, dev_id);
+ mutex_unlock(&port_mngr_grp_mtx);
+ mtk_port_stale_list_destroy(s_list);
+ } else {
+ s_list->dev_id = dev_id;
+ mutex_unlock(&port_mngr_grp_mtx);
+ }
+}
+
+void mtk_port_trb_init(struct mtk_port *port, struct trb *trb, enum mtk_trb_cmd_type cmd,
+ int (*trb_complete)(struct sk_buff *skb))
+{
+ kref_init(&trb->kref);
+ trb->channel_id = port->info.rx_ch;
+ trb->status = MTK_DFLT_TRB_STATUS;
+ trb->priv = port;
+ trb->cmd = cmd;
+ trb->trb_complete = trb_complete;
+}
+
+void mtk_port_trb_free(struct kref *trb_kref)
+{
+ struct trb *trb = container_of(trb_kref, struct trb, kref);
+ struct sk_buff *skb, *frag_skb, *next_skb;
+
+ skb = container_of((char *)trb, struct sk_buff, cb[0]);
+ /* Free frag_list for scatter gather TX */
+ if (trb->cmd == TRB_CMD_TX && skb_has_frag_list(skb)) {
+ frag_skb = skb_shinfo(skb)->frag_list;
+ while (frag_skb) {
+ next_skb = frag_skb->next;
+ frag_skb->next = NULL;
+ dev_kfree_skb_any(frag_skb);
+ frag_skb = next_skb;
+ }
+ skb_shinfo(skb)->frag_list = NULL;
+ skb->data_len = 0;
+ }
+ dev_kfree_skb_any(skb);
+}
+EXPORT_SYMBOL(mtk_port_trb_free);
+
+static int mtk_port_open_trb_complete(struct sk_buff *skb)
+{
+ struct trb_open_priv *trb_open_priv = (struct trb_open_priv *)skb->data;
+ struct trb *trb = (struct trb *)skb->cb;
+ struct mtk_port *port = trb->priv;
+
+ if (!trb->status) {
+ port->tx_mtu = trb_open_priv->tx_mtu;
+ port->rx_mtu = trb_open_priv->rx_mtu;
+ port->tx_frag_size = trb_open_priv->tx_frag_size;
+ port->rx_frag_size = trb_open_priv->rx_frag_size;
+ port->tx_mtu -= MTK_CCCI_H_ELEN;
+ port->rx_mtu -= MTK_CCCI_H_ELEN;
+ }
+
+ wake_up_all(&port->trb_wq);
+
+ kref_put(&trb->kref, mtk_port_trb_free);
+ return 0;
+}
+
+static int mtk_port_close_trb_complete(struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct mtk_port *port = trb->priv;
+
+ wake_up_all(&port->trb_wq);
+ wake_up_all(&port->rx_wq);
+ kref_put(&trb->kref, mtk_port_trb_free);
+
+ return 0;
+}
+
+static int mtk_port_tx_complete(struct sk_buff *skb)
+{
+ struct trb *trb = (struct trb *)skb->cb;
+ struct mtk_port *port = trb->priv;
+
+ if (trb->status < 0)
+ dev_warn(port->port_mngr->ctrl_blk->mdev->dev,
+ "Failed to send data: status:%d, port:%s\n",
+ trb->status, port->info.name);
+
+ wake_up_all(&port->trb_wq);
+ kref_put(&trb->kref, mtk_port_trb_free);
+
+ return 0;
+}
+
+int mtk_port_status_check(struct mtk_port *port)
+{
+ if (!test_bit(PORT_S_ENABLE, &port->status))
+ return -ENODEV;
+
+ if (!test_bit(PORT_S_OPEN, &port->status) || test_bit(PORT_S_FLUSH, &port->status) ||
+ !test_bit(PORT_S_WR, &port->status))
+ return -EBADF;
+
+ return 0;
+}
+
+int mtk_port_send_data(struct mtk_port *port, void *data)
+{
+ struct mtk_port_mngr *port_mngr;
+ struct sk_buff *skb = data;
+ bool force_send;
+ struct trb *trb;
+ int ret, len;
+
+ port_mngr = port->port_mngr;
+
+ force_send = !!(port->info.flags & (PORT_F_BLOCKING | PORT_F_FORCE_SEND));
+ trb = (struct trb *)skb->cb;
+ mtk_port_trb_init(port, trb, TRB_CMD_TX, mtk_port_tx_complete);
+ len = skb->len;
+ kref_get(&trb->kref); /* kref count 1->2 */
+
+ /* add ccci header */
+ mtk_port_add_header(skb);
+ ret = mtk_port_status_check(port);
+ if (!ret)
+ ret = port_mngr->ctrl_blk->ops->submit_skb(port_mngr->ctrl_blk->mdev,
+ skb, force_send);
+
+ if (ret < 0) {
+ kref_put(&trb->kref, mtk_port_trb_free); /* kref count 2->1 */
+ kref_put(&trb->kref, mtk_port_trb_free); /* kref count 1->0 */
+ port->tx_seq--;
+ goto out;
+ }
+
+ if (!(port->info.flags & PORT_F_BLOCKING)) {
+ kref_put(&trb->kref, mtk_port_trb_free);
+ ret = len;
+ goto out;
+ }
+start_wait:
+
+ /* wait trb done, and no timeout in tx blocking mode */
+ ret = wait_event_interruptible_timeout(port->trb_wq,
+ trb->status <= 0 ||
+ test_bit(PORT_S_FLUSH, &port->status) ||
+ !test_bit(PORT_S_WR, &port->status),
+ MTK_DFLT_TRB_TIMEOUT);
+ if (!ret) {
+ goto start_wait;
+ } else if (ret == -ERESTARTSYS) {
+ ret = -EINTR;
+ } else if (ret > 0) {
+ if (test_bit(PORT_S_FLUSH, &port->status))
+ ret = len;
+ else
+ ret = (!trb->status) ? len : trb->status;
+ }
+ kref_put(&trb->kref, mtk_port_trb_free);
+
+out:
+ return ret;
+}
+
+static int mtk_port_check_rx_seq(struct mtk_port *port, struct mtk_ccci_header *ccci_h)
+{
+ u16 seq_num, assert_bit, channel;
+ struct mtk_md_dev *mdev;
+
+ seq_num = FIELD_GET(MTK_HDR_FLD_SEQ, le32_to_cpu(ccci_h->status));
+ assert_bit = FIELD_GET(MTK_HDR_FLD_AST, le32_to_cpu(ccci_h->status));
+ if (assert_bit && port->rx_seq &&
+ ((seq_num - port->rx_seq) & MTK_CHECK_RX_SEQ_MASK) != 1) {
+ mdev = port->port_mngr->ctrl_blk->mdev;
+ channel = FIELD_GET(MTK_HDR_FLD_CHN, le32_to_cpu(ccci_h->status));
+ dev_warn(mdev->dev,
+ "<ch: %04x> seq num out-of-order %d->%d, len(%u)\n",
+ channel, seq_num, port->rx_seq,
+ le32_to_cpu(ccci_h->packet_len));
+
+ port->rx_seq = seq_num;
+ return -EPROTO;
+ }
+
+ return 0;
+}
+
+static int mtk_port_rx_dispatch_frag_skb(struct mtk_port *port, struct sk_buff *skb)
+{
+ struct sk_buff *frag_skb, *frag_next;
+ int ret;
+
+ frag_skb = skb_shinfo(skb)->frag_list;
+ skb->len -= skb->data_len;
+ skb->data_len = 0;
+ skb_shinfo(skb)->frag_list = NULL;
+
+ ret = ports_ops[port->info.type]->recv(port, skb);
+ if (ret < 0) {
+ skb_shinfo(skb)->frag_list = frag_skb;
+ return ret;
+ }
+
+ while (frag_skb) {
+ frag_next = frag_skb->next;
+ if (!frag_skb->len) {
+ frag_skb->next = NULL;
+ dev_kfree_skb_any(frag_skb);
+ frag_skb = frag_next;
+ continue;
+ }
+ frag_skb->next = NULL;
+ ret = ports_ops[port->info.type]->recv(port, frag_skb);
+ if (ret < 0) {
+ frag_skb->next = frag_next;
+ while (frag_skb) {
+ frag_next = frag_skb->next;
+ frag_skb->next = NULL;
+ dev_kfree_skb_any(frag_skb);
+ frag_skb = frag_next;
+ }
+ return -EIO;
+ }
+ frag_skb = frag_next;
+ }
+
+ return 0;
+}
+
+static int mtk_port_rx_dispatch(struct sk_buff *skb, void *priv, bool force_recv)
+{
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_ccci_header *ccci_h;
+ struct mtk_port *port = priv;
+ int ret = -EPROTO;
+ u16 channel;
+
+ if (!skb || !priv) {
+ pr_err("Invalid input value in rx dispatch\n");
+ return -EINVAL;
+ }
+
+ port_mngr = port->port_mngr;
+
+ ccci_h = mtk_port_strip_header(skb);
+ if (unlikely(!ccci_h)) {
+ dev_warn(port_mngr->ctrl_blk->mdev->dev,
+ "Unsupported: skb length(%d) is less than ccci header\n",
+ skb->len);
+ goto drop_data;
+ }
+
+ channel = FIELD_GET(MTK_HDR_FLD_CHN, le32_to_cpu(ccci_h->status));
+ port = mtk_port_search_by_id(port_mngr, channel);
+ if (unlikely(!port)) {
+ dev_warn(port_mngr->ctrl_blk->mdev->dev,
+ "Failed to find port by channel:%d\n", channel);
+ goto drop_data;
+ }
+
+ ret = mtk_port_check_rx_seq(port, ccci_h);
+ if (unlikely(ret))
+ goto drop_data;
+
+ port->rx_seq = FIELD_GET(MTK_HDR_FLD_SEQ, le32_to_cpu(ccci_h->status));
+ skb_pull(skb, sizeof(*ccci_h));
+
+ /* Support scatter gather transmission */
+ if (port->rx_mtu > port->rx_frag_size) {
+ ret = mtk_port_rx_dispatch_frag_skb(port, skb);
+ /* -EIO means partial data dispatch complete, does not goto drop flow */
+ if (ret < 0 && ret != -EIO)
+ goto drop_frag_skb;
+ } else {
+ ret = ports_ops[port->info.type]->recv(port, skb);
+ if (ret < 0)
+ goto drop_data;
+ }
+
+ return ret;
+
+drop_frag_skb:
+ {
+ struct sk_buff *frag_skb, *tmp;
+
+ frag_skb = skb_shinfo(skb)->frag_list;
+ while (frag_skb) {
+ tmp = frag_skb->next;
+ frag_skb->next = NULL;
+ dev_kfree_skb_any(frag_skb);
+ frag_skb = tmp;
+ }
+ skb_shinfo(skb)->frag_list = NULL;
+ }
+drop_data:
+ dev_kfree_skb_any(skb);
+ return ret;
+}
+
+int mtk_port_add_header(struct sk_buff *skb)
+{
+ struct mtk_ccci_header *ccci_h;
+ struct mtk_port *port;
+ struct trb *trb;
+
+ trb = (struct trb *)skb->cb;
+ if (trb->status == MTK_TRB_HEADER_ADDED)
+ return 0;
+
+ port = trb->priv;
+ if (!port)
+ return -EINVAL;
+
+ ccci_h = skb_push(skb, sizeof(*ccci_h));
+
+ ccci_h->packet_header = cpu_to_le32(0);
+ ccci_h->packet_len = cpu_to_le32(skb->len);
+ ccci_h->ex_msg = cpu_to_le32(0);
+ ccci_h->status = cpu_to_le32(FIELD_PREP(MTK_HDR_FLD_CHN, port->info.tx_ch) |
+ FIELD_PREP(MTK_HDR_FLD_SEQ, port->tx_seq++) |
+ FIELD_PREP(MTK_HDR_FLD_AST, 1));
+
+ trb->status = MTK_TRB_HEADER_ADDED;
+
+ return 0;
+}
+
+struct mtk_ccci_header *mtk_port_strip_header(struct sk_buff *skb)
+{
+ struct mtk_ccci_header *ccci_h;
+
+ if (skb->len < sizeof(*ccci_h)) {
+ pr_err("Invalid input value\n");
+ return NULL;
+ }
+
+ ccci_h = (struct mtk_ccci_header *)skb->data;
+
+ return ccci_h;
+}
+
+int mtk_port_status_update(struct mtk_md_dev *mdev, void *data, u32 data_len)
+{
+ struct mtk_port_enum_msg *msg = data;
+ struct mtk_port_info *port_info;
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_port *port;
+ int port_id;
+ u16 ch_id;
+
+ if (unlikely(!mdev || !msg))
+ return -EINVAL;
+
+ ctrl_blk = mdev->ctrl_blk;
+ port_mngr = ctrl_blk->port_mngr;
+ if (le16_to_cpu(msg->version) != MTK_PORT_ENUM_VER ||
+ le32_to_cpu(msg->head_pattern) != MTK_PORT_ENUM_HEAD_PATTERN ||
+ le32_to_cpu(msg->tail_pattern) != MTK_PORT_ENUM_TAIL_PATTERN)
+ return -EPROTO;
+
+ if (data_len < sizeof(*msg) +
+ le16_to_cpu(msg->port_cnt) * sizeof(*port_info))
+ return -EPROTO;
+
+ for (port_id = 0; port_id < le16_to_cpu(msg->port_cnt); port_id++) {
+ port_info = (struct mtk_port_info *)(msg->data +
+ (sizeof(*port_info) * port_id));
+ ch_id = FIELD_GET(MTK_INFO_FLD_CHID, le16_to_cpu(port_info->channel));
+ port = mtk_port_search_by_id(port_mngr, ch_id);
+ if (!port)
+ continue;
+ port->enable = FIELD_GET(MTK_INFO_FLD_EN, le16_to_cpu(port_info->channel));
+ }
+
+ return 0;
+}
+
+int mtk_port_ch_enable(struct mtk_port *port)
+{
+ struct mtk_port_mngr *port_mngr = port->port_mngr;
+ struct trb_open_priv *trb_open_priv;
+ struct sk_buff *skb;
+ struct trb *trb;
+ int ret;
+
+ skb = __dev_alloc_skb(Q_MTU_3_5K, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+
+ trb_open_priv = (struct trb_open_priv *)skb->data;
+ trb_open_priv->rx_done = mtk_port_rx_dispatch;
+
+ skb_put(skb, sizeof(struct trb_open_priv));
+ trb = (struct trb *)skb->cb;
+ mtk_port_trb_init(port, trb, TRB_CMD_ENABLE, mtk_port_open_trb_complete);
+ kref_get(&trb->kref);
+
+ ret = port_mngr->ctrl_blk->ops->submit_skb(port_mngr->ctrl_blk->mdev, skb, true);
+ if (ret) {
+ dev_err(port_mngr->ctrl_blk->mdev->dev,
+ "Failed to submit trb for port(%s), ret=%d\n",
+ port->info.name, ret);
+ kref_put(&trb->kref, mtk_port_trb_free);
+ kref_put(&trb->kref, mtk_port_trb_free);
+ return ret;
+ }
+
+ ret = wait_event_timeout(port->trb_wq, trb->status <= 0,
+ MTK_DFLT_TRB_TIMEOUT);
+ if (!ret)
+ ret = -ETIMEDOUT;
+ else
+ ret = trb->status;
+
+ kref_put(&trb->kref, mtk_port_trb_free);
+
+ return ret;
+}
+
+int mtk_port_ch_disable(struct mtk_port *port)
+{
+ struct mtk_port_mngr *port_mngr = port->port_mngr;
+ struct sk_buff *skb;
+ struct trb *trb;
+ int ret;
+
+ skb = __dev_alloc_skb(Q_MTU_3_5K, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+
+ trb = (struct trb *)skb->cb;
+ mtk_port_trb_init(port, trb, TRB_CMD_DISABLE, mtk_port_close_trb_complete);
+ kref_get(&trb->kref);
+
+ ret = port_mngr->ctrl_blk->ops->submit_skb(port_mngr->ctrl_blk->mdev, skb, true);
+ if (ret) {
+ dev_warn(port_mngr->ctrl_blk->mdev->dev,
+ "Failed to submit trb for port(%s), ret=%d\n",
+ port->info.name, ret);
+ kref_put(&trb->kref, mtk_port_trb_free);
+ kref_put(&trb->kref, mtk_port_trb_free);
+ return ret;
+ }
+
+ ret = wait_event_timeout(port->trb_wq, trb->status <= 0,
+ MTK_DFLT_TRB_TIMEOUT);
+ if (!ret)
+ ret = -ETIMEDOUT;
+ else
+ ret = trb->status;
+
+ kref_put(&trb->kref, mtk_port_trb_free);
+
+ return ret;
+}
+
+int mtk_port_mngr_init(struct mtk_ctrl_blk *ctrl_blk, struct mtk_port_cfg *port_cfg, int port_cnt)
+{
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_stale_list *s_list;
+ int ret = -ENOMEM;
+ int dev_id;
+
+ s_list = mtk_port_stale_list_init(ctrl_blk, &dev_id);
+ if (!s_list) {
+ dev_err((ctrl_blk->mdev)->dev, "Failed to init mtk_stale_list\n");
+ goto err_out;
+ }
+
+ port_mngr = devm_kzalloc(ctrl_blk->mdev->dev, sizeof(*port_mngr), GFP_KERNEL);
+ if (unlikely(!port_mngr)) {
+ dev_err((ctrl_blk->mdev)->dev, "Failed to alloc memory for port_mngr\n");
+ goto err_exit_stale_list;
+ }
+
+ port_mngr->ctrl_blk = ctrl_blk;
+ port_mngr->dev_id = dev_id;
+
+ ret = mtk_port_tbl_create(port_mngr, port_cfg, port_cnt, s_list);
+ if (unlikely(ret)) {
+ dev_err((ctrl_blk->mdev)->dev, "Failed to create port_tbl\n");
+ goto err_free_port_mngr;
+ }
+
+ ctrl_blk->port_mngr = port_mngr;
+
+ return ret;
+
+err_free_port_mngr:
+ mtk_port_tbl_destroy(port_mngr, s_list);
+err_exit_stale_list:
+ mtk_port_stale_list_exit(ctrl_blk, s_list, dev_id);
+err_out:
+ return ret;
+}
+
+void mtk_port_mngr_exit(struct mtk_ctrl_blk *ctrl_blk)
+{
+ struct mtk_port_mngr *port_mngr = ctrl_blk->port_mngr;
+ struct mtk_stale_list *s_list;
+ int dev_id;
+
+ s_list = mtk_port_stale_list_search(port_mngr->ctrl_blk->mdev->dev_str);
+ dev_id = port_mngr->dev_id;
+
+ mtk_port_tbl_destroy(port_mngr, s_list);
+
+ ctrl_blk->port_mngr = NULL;
+ mtk_port_stale_list_exit(ctrl_blk, s_list, dev_id);
+}
diff --git a/drivers/net/wwan/t9xx/mtk_port.h b/drivers/net/wwan/t9xx/mtk_port.h
new file mode 100644
index 000000000000..0a42313e348f
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_port.h
@@ -0,0 +1,159 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_PORT_H__
+#define __MTK_PORT_H__
+
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/radix-tree.h>
+#include <linux/skbuff.h>
+#include <linux/types.h>
+
+#include "mtk_ctrl_plane.h"
+#include "mtk_dev.h"
+
+#define MTK_PEER_ID_MASK (0xF000)
+#define MTK_PEER_ID_SHIFT (12)
+#define MTK_PEER_ID(ch) (((ch) & MTK_PEER_ID_MASK) >> MTK_PEER_ID_SHIFT)
+#define MTK_PEER_ID_SAP (0x1)
+#define MTK_PEER_ID_MD (0x2)
+#define MTK_CH_ID_MASK (0x0FFF)
+#define MTK_CH_ID(ch) ((ch) & MTK_CH_ID_MASK)
+#define MTK_DFLT_MAX_DEV_CNT (10)
+#define MTK_DFLT_PORT_NAME_LEN (20)
+
+/* Mapping MTK_PEER_ID and mtk_port_tbl index */
+#define MTK_PORT_TBL_TYPE(ch) (MTK_PEER_ID(ch) - 1)
+
+/* ccci header length + reserved space that is used in exception flow */
+#define MTK_CCCI_H_ELEN (128)
+
+#define MTK_HDR_FLD_AST ((u32)BIT(31))
+#define MTK_HDR_FLD_SEQ GENMASK(30, 16)
+#define MTK_HDR_FLD_CHN GENMASK(15, 0)
+
+#define MTK_INFO_FLD_EN ((u16)BIT(15))
+#define MTK_INFO_FLD_CHID GENMASK(14, 0)
+
+enum mtk_port_status {
+ PORT_S_DFLT = 0,
+ PORT_S_ENABLE,
+ PORT_S_OPEN,
+ PORT_S_RD,
+ PORT_S_WR,
+ PORT_S_FLUSH,
+ PORT_S_ON_STALE_LIST,
+ PORT_S_STOP,
+};
+
+enum mtk_ccci_ch {
+ /* to sAP */
+ CCCI_SAP_CONTROL_RX = 0x1000,
+ CCCI_SAP_CONTROL_TX = 0x1001,
+ /* to MD */
+ CCCI_CONTROL_RX = 0x2000,
+ CCCI_CONTROL_TX = 0x2001,
+};
+
+enum mtk_port_flag {
+ PORT_F_DFLT = 0,
+ PORT_F_BLOCKING = BIT(1),
+ PORT_F_ALLOW_DROP = BIT(2),
+ PORT_F_FORCE_SEND = BIT(6),
+};
+
+enum mtk_port_tbl {
+ PORT_TBL_SAP,
+ PORT_TBL_MD,
+ PORT_TBL_MAX
+};
+
+enum mtk_port_type {
+ PORT_TYPE_INTERNAL,
+ PORT_TYPE_MAX
+};
+
+struct mtk_internal_port {
+ void *arg;
+ int (*recv_cb)(void *arg, struct sk_buff *skb);
+};
+
+struct mtk_port_cfg {
+ enum mtk_ccci_ch tx_ch;
+ enum mtk_ccci_ch rx_ch;
+ enum mtk_port_type type;
+ char name[MTK_DFLT_PORT_NAME_LEN];
+ unsigned char flags;
+};
+
+struct mtk_port {
+ struct mtk_port_cfg info;
+ struct kref kref;
+ bool enable;
+ unsigned long status;
+ unsigned int minor;
+ unsigned short tx_seq;
+ unsigned short rx_seq;
+ unsigned int tx_mtu;
+ unsigned int rx_mtu;
+ u32 tx_frag_size;
+ u32 rx_frag_size;
+ struct sk_buff_head rx_skb_list;
+ unsigned int rx_data_len;
+ unsigned int rx_buf_size;
+ wait_queue_head_t trb_wq;
+ wait_queue_head_t rx_wq;
+ struct list_head stale_entry;
+ char dev_str[MTK_DEV_STR_LEN];
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_internal_port i_priv;
+};
+
+struct mtk_port_mngr {
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct radix_tree_root port_tbl[PORT_TBL_MAX];
+ unsigned int port_cnt;
+ int dev_id;
+};
+
+struct mtk_stale_list {
+ struct list_head entry;
+ struct list_head ports;
+ char dev_str[MTK_DEV_STR_LEN];
+ int dev_id;
+ rwlock_t port_mngr_lock;
+};
+
+struct mtk_ccci_header {
+ __le32 packet_header;
+ __le32 packet_len;
+ __le32 status;
+ __le32 ex_msg;
+};
+
+struct mtk_port_layer_cfg {
+ struct mtk_port_cfg *port_cfg;
+ int port_cnt;
+};
+
+extern const struct port_ops *ports_ops[PORT_TYPE_MAX];
+
+void mtk_port_release(struct kref *port_kref);
+void mtk_port_trb_free(struct kref *trb_kref);
+struct mtk_port *mtk_port_search_by_name(struct mtk_port_mngr *port_mngr, char *name);
+void mtk_port_stale_list_grp_cleanup(void);
+int mtk_port_add_header(struct sk_buff *skb);
+struct mtk_ccci_header *mtk_port_strip_header(struct sk_buff *skb);
+int mtk_port_status_check(struct mtk_port *port);
+int mtk_port_send_data(struct mtk_port *port, void *data);
+int mtk_port_status_update(struct mtk_md_dev *mdev, void *data, u32 data_len);
+int mtk_port_ch_enable(struct mtk_port *port);
+int mtk_port_ch_disable(struct mtk_port *port);
+int mtk_port_mngr_init(struct mtk_ctrl_blk *ctrl_blk, struct mtk_port_cfg *port_cfg, int port_cnt);
+void mtk_port_mngr_exit(struct mtk_ctrl_blk *ctrl_blk);
+void mtk_port_trb_init(struct mtk_port *port, struct trb *trb, enum mtk_trb_cmd_type cmd,
+ int (*trb_complete)(struct sk_buff *skb));
+#endif /* __MTK_PORT_H__ */
diff --git a/drivers/net/wwan/t9xx/mtk_port_io.c b/drivers/net/wwan/t9xx/mtk_port_io.c
new file mode 100644
index 000000000000..a4e264cd5f63
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_port_io.c
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+#include <linux/netdevice.h>
+
+#include "mtk_port_io.h"
+
+static int mtk_port_get_locked(struct mtk_port *port)
+{
+ int ret = 0;
+
+ mutex_lock(&port_mngr_grp_mtx);
+ if (!port) {
+ mutex_unlock(&port_mngr_grp_mtx);
+ pr_err("Port does not exist\n");
+ return -ENODEV;
+ }
+ kref_get(&port->kref);
+ mutex_unlock(&port_mngr_grp_mtx);
+
+ return ret;
+}
+
+static void mtk_port_put_locked(struct mtk_port *port)
+{
+ mutex_lock(&port_mngr_grp_mtx);
+ kref_put(&port->kref, mtk_port_release);
+ mutex_unlock(&port_mngr_grp_mtx);
+}
+
+static void mtk_port_struct_init(struct mtk_port *port)
+{
+ port->tx_seq = 0;
+ port->rx_seq = -1;
+ clear_bit(PORT_S_ENABLE, &port->status);
+ kref_init(&port->kref);
+ skb_queue_head_init(&port->rx_skb_list);
+ port->rx_buf_size = MTK_RX_BUF_SIZE;
+ init_waitqueue_head(&port->trb_wq);
+ init_waitqueue_head(&port->rx_wq);
+}
+
+static int mtk_port_internal_init(struct mtk_port *port)
+{
+ mtk_port_struct_init(port);
+ port->enable = false;
+
+ return 0;
+}
+
+static void mtk_port_internal_exit(struct mtk_port *port)
+{
+ if (test_bit(PORT_S_ENABLE, &port->status))
+ ports_ops[port->info.type]->disable(port);
+}
+
+static void mtk_port_reset(struct mtk_port *port)
+{
+ port->tx_seq = 0;
+ port->rx_seq = -1;
+}
+
+static void mtk_port_internal_enable(struct mtk_port *port)
+{
+ int ret;
+
+ if (test_bit(PORT_S_ENABLE, &port->status))
+ return;
+
+ ret = mtk_port_ch_enable(port);
+ if (ret && ret != -EBUSY)
+ return;
+
+ set_bit(PORT_S_WR, &port->status);
+ set_bit(PORT_S_ENABLE, &port->status);
+}
+
+static void mtk_port_internal_disable(struct mtk_port *port)
+{
+ if (!test_and_clear_bit(PORT_S_ENABLE, &port->status))
+ return;
+
+ clear_bit(PORT_S_WR, &port->status);
+ mtk_port_ch_disable(port);
+}
+
+static int mtk_port_internal_recv(struct mtk_port *port, struct sk_buff *skb)
+{
+ struct mtk_internal_port *priv;
+ int ret = -ENXIO;
+
+ if (!test_bit(PORT_S_OPEN, &port->status))
+ goto drop_data;
+
+ priv = &port->i_priv;
+ if (!priv->recv_cb || !priv->arg)
+ goto drop_data;
+
+ ret = priv->recv_cb(priv->arg, skb);
+ return ret;
+
+drop_data:
+ return ret;
+}
+
+static int mtk_port_common_open(struct mtk_port *port)
+{
+ int ret = 0;
+
+ if (!test_bit(PORT_S_ENABLE, &port->status))
+ return -ENODEV;
+
+ if (test_bit(PORT_S_OPEN, &port->status))
+ return -EBUSY;
+
+ skb_queue_purge(&port->rx_skb_list);
+ set_bit(PORT_S_OPEN, &port->status);
+ clear_bit(PORT_S_FLUSH, &port->status);
+
+ return ret;
+}
+
+static void mtk_port_common_close(struct mtk_port *port)
+{
+ clear_bit(PORT_S_OPEN, &port->status);
+
+ skb_queue_purge(&port->rx_skb_list);
+ port->rx_data_len = 0;
+
+ set_bit(PORT_S_FLUSH, &port->status);
+ wake_up_all(&port->trb_wq);
+ wake_up_all(&port->rx_wq);
+}
+
+void *mtk_port_internal_open(struct mtk_md_dev *mdev, char *name, int flag)
+{
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_port *port;
+ int ret;
+
+ ctrl_blk = mdev->ctrl_blk;
+ port_mngr = ctrl_blk->port_mngr;
+
+ port = mtk_port_search_by_name(port_mngr, name);
+ if (port && port->info.type != PORT_TYPE_INTERNAL) {
+ port = NULL;
+ goto out;
+ }
+
+ ret = mtk_port_get_locked(port);
+ if (ret)
+ goto out;
+
+ ret = mtk_port_common_open(port);
+ if (ret) {
+ mtk_port_put_locked(port);
+ port = NULL;
+ goto out;
+ }
+
+ if (flag & O_NONBLOCK)
+ port->info.flags &= ~PORT_F_BLOCKING;
+ else
+ port->info.flags |= PORT_F_BLOCKING;
+out:
+ return port;
+}
+
+int mtk_port_internal_close(void *i_port)
+{
+ struct mtk_port *port = i_port;
+ int ret = 0;
+
+ if (!port) {
+ ret = -EINVAL;
+ goto end;
+ }
+
+ if (!test_bit(PORT_S_OPEN, &port->status)) {
+ pr_err("Port(%s) has been closed\n", port->info.name);
+ ret = -EBADF;
+ goto end;
+ }
+
+ mtk_port_common_close(port);
+ mtk_port_put_locked(port);
+end:
+ return ret;
+}
+
+int mtk_port_internal_write(void *i_port, struct sk_buff *skb)
+{
+ struct mtk_port *port = i_port;
+
+ if (!port || !skb) {
+ if (skb)
+ dev_kfree_skb_any(skb);
+ pr_err_ratelimited("Internal write: invalid input\n");
+ return -EINVAL;
+ }
+ return mtk_port_send_data(port, skb);
+}
+
+void mtk_port_internal_recv_register(void *i_port,
+ int (*cb)(void *priv, struct sk_buff *skb),
+ void *arg)
+{
+ struct mtk_port *port = i_port;
+ struct mtk_internal_port *priv;
+
+ priv = &port->i_priv;
+ priv->arg = arg;
+ priv->recv_cb = cb;
+}
+
+int mtk_port_io_init(void)
+{
+ return 0;
+}
+
+void mtk_port_io_exit(void)
+{
+}
+
+static const struct port_ops port_internal_ops = {
+ .init = mtk_port_internal_init,
+ .exit = mtk_port_internal_exit,
+ .reset = mtk_port_reset,
+ .enable = mtk_port_internal_enable,
+ .disable = mtk_port_internal_disable,
+ .recv = mtk_port_internal_recv,
+};
+
+const struct port_ops *ports_ops[PORT_TYPE_MAX] = {
+ &port_internal_ops,
+};
diff --git a/drivers/net/wwan/t9xx/mtk_port_io.h b/drivers/net/wwan/t9xx/mtk_port_io.h
new file mode 100644
index 000000000000..0c10e893b7e0
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_port_io.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_PORT_IO_H__
+#define __MTK_PORT_IO_H__
+
+#include <linux/skbuff.h>
+
+#include "mtk_port.h"
+
+#define MTK_RX_BUF_SIZE (1024 * 1024)
+
+extern struct mutex port_mngr_grp_mtx;
+
+struct port_ops {
+ int (*init)(struct mtk_port *port);
+ void (*exit)(struct mtk_port *port);
+ void (*reset)(struct mtk_port *port);
+ void (*enable)(struct mtk_port *port);
+ void (*disable)(struct mtk_port *port);
+ int (*recv)(struct mtk_port *port, struct sk_buff *skb);
+};
+
+void *mtk_port_internal_open(struct mtk_md_dev *mdev, char *name, int flag);
+int mtk_port_internal_close(void *i_port);
+int mtk_port_internal_write(void *i_port, struct sk_buff *skb);
+void mtk_port_internal_recv_register(void *i_port,
+ int (*cb)(void *priv, struct sk_buff *skb),
+ void *arg);
+
+int mtk_port_io_init(void);
+void mtk_port_io_exit(void);
+
+#endif /* __MTK_PORT_IO_H__ */
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
index c1bb787ee981..8611561dd67c 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
@@ -4,6 +4,7 @@
*/
#include "mtk_cldma.h"
+#include "mtk_port.h"
#include "mtk_trans_ctrl.h"
#define TRB_SRV_NUM (1)
@@ -13,12 +14,34 @@ static const int mtk_srv_cfg_m9xx[NR_CLDMA][HW_QUE_NUM] = {
{0},
};
+/* the number of RX GPDs should be at last two */
static const struct queue_info mtk_queue_info_m9xx[] = {
+ {CCCI_CONTROL_TX, CCCI_CONTROL_RX, CLDMA1, TXQ(0), RXQ(0),
+ Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
+ {CCCI_SAP_CONTROL_TX, CCCI_SAP_CONTROL_RX, CLDMA0, TXQ(0), RXQ(0),
+ Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
+};
+
+static const struct mtk_port_cfg port_cfg_m9xx[] = {
+ {CCCI_CONTROL_TX, CCCI_CONTROL_RX, PORT_TYPE_INTERNAL, "MDCTRL",
+ PORT_F_ALLOW_DROP},
+ {CCCI_SAP_CONTROL_TX, CCCI_SAP_CONTROL_RX, PORT_TYPE_INTERNAL, "SAPCTRL",
+ PORT_F_ALLOW_DROP},
+};
+
+static struct mtk_port_layer_cfg port_layer_cfg_m9xx = {
+ .port_cfg = (struct mtk_port_cfg *)port_cfg_m9xx,
+ .port_cnt = ARRAY_SIZE(port_cfg_m9xx),
+};
+
+static struct mtk_ctrl_cfg mtk_ctrl_cfg_m9xx = {
+ .port_layer_cfg = &port_layer_cfg_m9xx,
};
struct mtk_ctrl_info mtk_ctrl_info_m9xx = {
+ .ctrl_cfg = &mtk_ctrl_cfg_m9xx,
+ .srv_cfg = (int **)mtk_srv_cfg_m9xx,
.queue_info = (struct queue_info *)mtk_queue_info_m9xx,
.queue_info_num = ARRAY_SIZE(mtk_queue_info_m9xx),
- .srv_cfg = (int **)mtk_srv_cfg_m9xx,
.trb_srv_num = TRB_SRV_NUM,
};
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
index da2a6926b801..dbc17da590bc 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
@@ -17,6 +17,8 @@
#include "mtk_trans_ctrl.h"
#include "mtk_pci.h"
#include "mtk_pci_reg.h"
+#include "mtk_port.h"
+#include "mtk_port_io.h"
#define MTK_PCI_BAR_NUM 6
#define MTK_PCI_TRANSPARENT_ATR_SIZE (0x3F)
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
index 55297cfcdbc6..6a0a415a556e 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
@@ -16,12 +16,13 @@
#include "mtk_ctrl_plane.h"
#include "mtk_dev.h"
#include "mtk_pci.h"
+#include "mtk_port.h"
#include "mtk_trans_ctrl.h"
#define MTK_DFLT_PORT_NAME_LEN (20)
static struct mtk_ctrl_info_desc mtk_ctrl_info_tbl[] = {
- {2304, &ctrl_info_name(m9xx)},
+ {0x01CA, &ctrl_info_name(m9xx)},
{0, NULL},
};
@@ -131,6 +132,7 @@ static void mtk_ctrl_trb_handler(struct trb_srv *srv, struct trans_list *trans_l
if (!skb)
break;
trb = (struct trb *)skb->cb;
+ kref_get(&trb->kref);
switch (trb->cmd) {
case TRB_CMD_ENABLE:
@@ -150,8 +152,10 @@ static void mtk_ctrl_trb_handler(struct trb_srv *srv, struct trans_list *trans_l
kick = true;
break;
}
- if (err == -EAGAIN)
+ if (err == -EAGAIN) {
+ kref_put(&trb->kref, mtk_port_trb_free);
return;
+ }
skb_unlink(skb, skb_list);
trb->status = err;
@@ -182,6 +186,8 @@ static void mtk_ctrl_trb_handler(struct trb_srv *srv, struct trans_list *trans_l
kick = false;
}
+ kref_put(&trb->kref, mtk_port_trb_free);
+
loop++;
} while (loop < TRB_NUM_PER_ROUND);
}
@@ -492,6 +498,7 @@ static void mtk_trans_get_ctrl_info(struct mtk_ctrl_cfg *cfg,
continue;
ctrl_info = ctrl_info_desc->ctrl_info;
+ cfg->port_layer_cfg = ctrl_info->ctrl_cfg->port_layer_cfg;
memcpy(trans->srv_cfg, ctrl_info->srv_cfg,
sizeof(int) * NR_CLDMA * HW_QUE_NUM);
trans->queue_info = ctrl_info->queue_info;
@@ -504,6 +511,7 @@ int mtk_trans_ctrl_init(struct mtk_md_dev *mdev)
{
struct mtk_ctrl_trans *trans;
struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_ctrl_cfg *cfg;
int err;
trans = devm_kzalloc(mdev->dev, sizeof(*trans), GFP_KERNEL);
@@ -512,15 +520,19 @@ int mtk_trans_ctrl_init(struct mtk_md_dev *mdev)
trans->mdev = mdev;
trans->queues_cnt = 0;
- mtk_trans_get_ctrl_info(NULL, trans, mdev->hw_ver);
- if (!trans->queue_info ||
+ cfg = devm_kzalloc(mdev->dev, sizeof(*cfg), GFP_KERNEL);
+ if (!cfg)
+ return -ENOMEM;
+
+ mtk_trans_get_ctrl_info(cfg, trans, mdev->hw_ver);
+ if (!cfg->port_layer_cfg || !trans->queue_info ||
trans->trb_srv_num <= 0 || trans->trb_srv_num > TRB_SRV_MAX_NUM ||
trans->queue_info_num <= 0) {
dev_err(mdev->dev, "Failed to get ctrl info!\n");
goto err_free_cfg;
}
- err = mtk_ctrl_init(mdev, &pcie_ctrl_ops);
+ err = mtk_ctrl_init(mdev, &pcie_ctrl_ops, cfg);
if (err)
goto err_free_cfg;
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
index 0d25d1f51671..a3ff56ddf86f 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
@@ -12,6 +12,7 @@
#include <linux/types.h>
#include "mtk_dev.h"
+#include "mtk_port.h"
#define TRB_SRV_MAX_NUM (1)
#define HW_QUE_NUM (8)
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
` (3 preceding siblings ...)
2026-08-11 7:14 ` [PATCH v6 4/7] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
` (2 subsequent siblings)
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
The FSM (Finite-state Machine) thread is responsible for
synchronizing the actions of different modules. The
asynchronous events from the device or the OS will trigger
a state transition.
The FSM thread will append it to the event queue when an
event arrives. It handles the events sequentially. After
processing the event, the FSM thread notifies other modules
before and after the state transition.
Seven FSM states are defined. They can transition from one
state to another, self-transition in some states, and
transition in some sub-states.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/t9xx/Makefile | 3 +-
drivers/net/wwan/t9xx/mtk_ctrl_plane.c | 46 ++
drivers/net/wwan/t9xx/mtk_ctrl_plane.h | 2 +
drivers/net/wwan/t9xx/mtk_dev.h | 1 +
drivers/net/wwan/t9xx/mtk_fsm.c | 965 ++++++++++++++++++++++++
drivers/net/wwan/t9xx/mtk_fsm.h | 140 ++++
drivers/net/wwan/t9xx/mtk_port.c | 63 ++
drivers/net/wwan/t9xx/mtk_port.h | 2 +
drivers/net/wwan/t9xx/mtk_utility.h | 33 +
drivers/net/wwan/t9xx/pcie/mtk_cldma.c | 222 +++++-
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h | 3 -
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c | 7 +-
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h | 2 -
drivers/net/wwan/t9xx/pcie/mtk_pci.c | 16 +-
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c | 10 +
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h | 1 -
16 files changed, 1500 insertions(+), 16 deletions(-)
diff --git a/drivers/net/wwan/t9xx/Makefile b/drivers/net/wwan/t9xx/Makefile
index db3b1aa1928b..75760b2039dc 100644
--- a/drivers/net/wwan/t9xx/Makefile
+++ b/drivers/net/wwan/t9xx/Makefile
@@ -10,4 +10,5 @@ mtk_t9xx-y := \
mtk_dev.o \
mtk_ctrl_plane.o \
mtk_port.o \
- mtk_port_io.o
+ mtk_port_io.o \
+ mtk_fsm.o
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
index 6ed47d074556..1d9dc5348240 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
@@ -5,10 +5,46 @@
*/
#include <linux/device.h>
+#include <linux/freezer.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/pm_runtime.h>
+#include <linux/sched.h>
+#include <linux/wait.h>
#include "mtk_ctrl_plane.h"
#include "mtk_port.h"
+#define TAG "CTRL"
+
+static void mtk_ctrl_trans_fsm_state_handler(struct mtk_fsm_param *param,
+ struct mtk_ctrl_blk *ctrl_blk)
+{
+ struct mtk_md_dev *mdev = ctrl_blk->mdev;
+
+ switch (param->to) {
+ case FSM_STATE_OFF:
+ ctrl_blk->ops->fsm_indication(mdev, param);
+ ctrl_blk->ops->exit(mdev);
+ break;
+ case FSM_STATE_ON:
+ ctrl_blk->ops->init(mdev);
+ fallthrough;
+ default:
+ ctrl_blk->ops->fsm_indication(mdev, param);
+ break;
+ }
+}
+
+static void mtk_ctrl_fsm_state_listener(struct mtk_fsm_param *param, void *data)
+{
+ struct mtk_ctrl_blk *ctrl_blk = data;
+
+ mtk_port_mngr_fsm_state_handler(param, ctrl_blk->port_mngr);
+ mtk_ctrl_trans_fsm_state_handler(param, ctrl_blk);
+ mtk_port_mngr_fsm_state_handler_late(param, ctrl_blk->port_mngr);
+}
+
/**
* mtk_ctrl_init() - Initialize the control plane block.
* @mdev: Pointer to the MTK modem device.
@@ -39,8 +75,17 @@ int mtk_ctrl_init(struct mtk_md_dev *mdev, struct mtk_ctrl_hif_ops *ops, struct
if (err)
goto err_free_mem;
+ err = mtk_fsm_notifier_register(mdev, MTK_USER_CTRL, mtk_ctrl_fsm_state_listener,
+ ctrl_blk, FSM_PRIO_1, false);
+ if (err) {
+ dev_err(mdev->dev, "Fail to register fsm notification(ret = %d)\n", err);
+ goto err_port_exit;
+ }
+
return 0;
+err_port_exit:
+ mtk_port_mngr_exit(ctrl_blk);
err_free_mem:
return err;
}
@@ -56,6 +101,7 @@ void mtk_ctrl_exit(struct mtk_md_dev *mdev)
{
struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ mtk_fsm_notifier_unregister(mdev, MTK_USER_CTRL);
mtk_port_mngr_exit(ctrl_blk);
mdev->ctrl_blk = NULL;
}
diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
index d7fcccde8a1b..92817e92a2e4 100644
--- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
+++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.h
@@ -10,6 +10,7 @@
#include <linux/skbuff.h>
#include "mtk_dev.h"
+#include "mtk_fsm.h"
#define Q_MTU_2K (0x800)
#define Q_MTU_3_5K (0xE00)
@@ -62,6 +63,7 @@ struct mtk_ctrl_hif_ops {
int (*init)(struct mtk_md_dev *mdev);
int (*exit)(struct mtk_md_dev *mdev);
int (*submit_skb)(struct mtk_md_dev *mdev, struct sk_buff *skb, bool force_send);
+ void (*fsm_indication)(struct mtk_md_dev *mdev, struct mtk_fsm_param *param);
int (*send_cmd)(struct mtk_md_dev *mdev, int cmd, void *data);
};
diff --git a/drivers/net/wwan/t9xx/mtk_dev.h b/drivers/net/wwan/t9xx/mtk_dev.h
index 9843171fe2ae..b4777b7c5d49 100644
--- a/drivers/net/wwan/t9xx/mtk_dev.h
+++ b/drivers/net/wwan/t9xx/mtk_dev.h
@@ -59,6 +59,7 @@ struct mtk_md_dev {
u32 hw_ver;
char dev_str[MTK_DEV_STR_LEN];
struct mtk_ctrl_blk *ctrl_blk;
+ struct mtk_md_fsm *fsm;
};
static inline u32 mtk_dev_get_dev_state(struct mtk_md_dev *mdev)
diff --git a/drivers/net/wwan/t9xx/mtk_fsm.c b/drivers/net/wwan/t9xx/mtk_fsm.c
new file mode 100644
index 000000000000..8cf9b239caea
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_fsm.c
@@ -0,0 +1,965 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/kref.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/pci.h>
+#include <linux/sched/signal.h>
+#include <linux/skbuff.h>
+#include <linux/wait.h>
+
+#include "mtk_fsm.h"
+#include "mtk_port.h"
+#include "mtk_port_io.h"
+#include "mtk_utility.h"
+
+#define EVT_TF_GATECLOSED (1)
+#define MTK_FSM_INFO_LEN (64)
+
+#define FSM_HS_START_MASK (FSM_F_SAP_HS_START | FSM_F_MD_HS_START)
+#define FSM_HS2_DONE_MASK (FSM_F_SAP_HS2_DONE | FSM_F_MD_HS2_DONE)
+
+#define RTFT_DATA_SIZE (3 * 1024)
+#define EVT_HANDLER_TIMEOUT (HZ * 30)
+#define BLOCKING_EVT_TIMEOUT (2 * EVT_HANDLER_TIMEOUT)
+
+#define REGION_BITMASK 0xF
+#define DEVICE_CFG_SHIFT 24
+#define DEVICE_CFG_REGION_MASK 0x3
+
+enum device_stage {
+ DEV_STAGE_IDLE = 4,
+ DEV_STAGE_MAX
+};
+
+enum device_cfg {
+ DEV_CFG_NORMAL = 0,
+ DEV_CFG_MD_ONLY,
+};
+
+enum runtime_feature_support_type {
+ RTFT_TYPE_NOT_EXIST = 0,
+ RTFT_TYPE_NOT_SUPPORT = 1,
+ RTFT_TYPE_MUST_SUPPORT = 2,
+ RTFT_TYPE_OPTIONAL_SUPPORT = 3,
+ RTFT_TYPE_SUPPORT_BACKWARD_COMPAT = 4,
+};
+
+enum query_runtime_feature_id {
+ QUERY_RTFT_ID_MD_PORT_ENUM = 0,
+ QUERY_RTFT_ID_SAP_PORT_ENUM = 1,
+ QUERY_RTFT_ID_MD_PORT_CFG = 2,
+ QUERY_RTFT_ID_MAX
+};
+
+enum ctrl_msg_id {
+ CTRL_MSG_HS1 = 0,
+ CTRL_MSG_HS2 = 1,
+ CTRL_MSG_HS3 = 2,
+};
+
+struct ctrl_msg_header {
+ __le32 id;
+ __le32 ex_msg;
+ __le32 data_len;
+ u8 reserved[];
+} __packed;
+
+struct runtime_feature_entry {
+ u8 feature_id;
+ struct runtime_feature_info support_info;
+ u8 reserved[2];
+ __le32 data_len;
+ u8 data[];
+};
+
+struct feature_query {
+ __le32 head_pattern;
+ struct runtime_feature_info ft_set[FEATURE_CNT];
+ __le32 tail_pattern;
+};
+
+static int mtk_fsm_send_hs1_msg(struct fsm_hs_info *hs_info)
+{
+ struct ctrl_msg_header *ctrl_msg_h;
+ struct feature_query *ft_query;
+ struct sk_buff *skb;
+ int ret, msg_size;
+
+ msg_size = sizeof(*ctrl_msg_h) + sizeof(*ft_query);
+ skb = __dev_alloc_skb(msg_size, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+
+ skb_put(skb, msg_size);
+ ctrl_msg_h = (struct ctrl_msg_header *)skb->data;
+ ctrl_msg_h->id = cpu_to_le32(CTRL_MSG_HS1);
+ ctrl_msg_h->ex_msg = 0;
+ ctrl_msg_h->data_len = cpu_to_le32(sizeof(*ft_query));
+
+ ft_query = (struct feature_query *)(skb->data + sizeof(*ctrl_msg_h));
+ ft_query->head_pattern = cpu_to_le32(FEATURE_QUERY_PATTERN);
+ memcpy(ft_query->ft_set, hs_info->query_ft_set, sizeof(hs_info->query_ft_set));
+ ft_query->tail_pattern = cpu_to_le32(FEATURE_QUERY_PATTERN);
+
+ /* send handshake1 message to device */
+ ret = mtk_port_internal_write(hs_info->ctrl_port, skb);
+ if (ret <= 0)
+ return ret;
+
+ return 0;
+}
+
+static int mtk_fsm_feature_set_match(enum runtime_feature_support_type *cur_ft_spt,
+ struct runtime_feature_info rtft_info_st,
+ struct runtime_feature_info rtft_info_cfg)
+{
+ int ret = 0;
+
+ switch (FIELD_GET(FEATURE_TYPE, rtft_info_st.feature)) {
+ case RTFT_TYPE_NOT_EXIST:
+ fallthrough;
+ case RTFT_TYPE_NOT_SUPPORT:
+ *cur_ft_spt = RTFT_TYPE_NOT_EXIST;
+ break;
+ case RTFT_TYPE_MUST_SUPPORT:
+ if (FIELD_GET(FEATURE_TYPE, rtft_info_cfg.feature) == RTFT_TYPE_NOT_EXIST ||
+ FIELD_GET(FEATURE_TYPE, rtft_info_cfg.feature) == RTFT_TYPE_NOT_SUPPORT)
+ ret = -EPROTO;
+ else
+ *cur_ft_spt = RTFT_TYPE_MUST_SUPPORT;
+ break;
+ case RTFT_TYPE_OPTIONAL_SUPPORT:
+ if (FIELD_GET(FEATURE_TYPE, rtft_info_cfg.feature) == RTFT_TYPE_NOT_EXIST ||
+ FIELD_GET(FEATURE_TYPE, rtft_info_cfg.feature) == RTFT_TYPE_NOT_SUPPORT) {
+ *cur_ft_spt = RTFT_TYPE_NOT_SUPPORT;
+ } else {
+ if (FIELD_GET(FEATURE_VER, rtft_info_st.feature) ==
+ FIELD_GET(FEATURE_VER, rtft_info_cfg.feature))
+ *cur_ft_spt = RTFT_TYPE_MUST_SUPPORT;
+ else
+ *cur_ft_spt = RTFT_TYPE_NOT_SUPPORT;
+ }
+ break;
+ case RTFT_TYPE_SUPPORT_BACKWARD_COMPAT:
+ if (FIELD_GET(FEATURE_VER, rtft_info_st.feature) >=
+ FIELD_GET(FEATURE_VER, rtft_info_cfg.feature))
+ *cur_ft_spt = RTFT_TYPE_MUST_SUPPORT;
+ else
+ *cur_ft_spt = RTFT_TYPE_NOT_EXIST;
+ break;
+ default:
+ ret = -EPROTO;
+ }
+
+ return ret;
+}
+
+static int (*query_rtft_action[FEATURE_CNT])(struct mtk_md_dev *mdev,
+ void *rt_data, u32 data_len) = {
+ [QUERY_RTFT_ID_MD_PORT_ENUM] = mtk_port_status_update,
+ [QUERY_RTFT_ID_SAP_PORT_ENUM] = mtk_port_status_update,
+};
+
+static int mtk_fsm_parse_hs2_msg(struct fsm_hs_info *hs_info)
+{
+ struct mtk_md_fsm *fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
+ char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
+ enum runtime_feature_support_type cur_ft_spt;
+ struct runtime_feature_entry *rtft_entry;
+ unsigned int ft_id, offset, data_len;
+ int ret = 0;
+
+ offset = sizeof(struct feature_query);
+ for (ft_id = 0; ft_id < FEATURE_CNT; ft_id++) {
+ if (offset + sizeof(*rtft_entry) > hs_info->rt_data_len)
+ break;
+
+ rtft_entry = (struct runtime_feature_entry *)(rt_data + offset);
+ ret = mtk_fsm_feature_set_match(&cur_ft_spt,
+ rtft_entry->support_info,
+ hs_info->query_ft_set[ft_id]);
+ if (ret < 0)
+ break;
+
+ data_len = le32_to_cpu(rtft_entry->data_len);
+ if (data_len > hs_info->rt_data_len - offset - sizeof(*rtft_entry))
+ break;
+
+ if (cur_ft_spt == RTFT_TYPE_MUST_SUPPORT)
+ if (query_rtft_action[ft_id])
+ ret = query_rtft_action[ft_id](fsm->mdev,
+ rtft_entry->data,
+ data_len);
+ if (ret < 0)
+ break;
+
+ offset += sizeof(*rtft_entry) + data_len;
+ }
+
+ if (ft_id != FEATURE_CNT) {
+ dev_err((fsm->mdev)->dev, "Unable to handle mistake hs2 msg, ft_id=%d\n", ft_id);
+ ret = -EPROTO;
+ }
+
+ return ret;
+}
+
+static int mtk_fsm_append_rtft_entries(struct mtk_md_dev *mdev, void *feature_data,
+ unsigned int *len, struct fsm_hs_info *hs_info)
+{
+ char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
+ struct runtime_feature_entry *rtft_entry;
+ int ft_id, ret = 0, rtdata_len = 0;
+ struct feature_query *ft_query;
+
+ ft_query = (struct feature_query *)rt_data;
+ if (le32_to_cpu(ft_query->head_pattern) != FEATURE_QUERY_PATTERN ||
+ le32_to_cpu(ft_query->tail_pattern) != FEATURE_QUERY_PATTERN) {
+ ret = -EPROTO;
+ goto hs_err;
+ }
+
+ /* parse runtime feature query and fill runtime feature entry */
+ rtft_entry = feature_data;
+ for (ft_id = 0; ft_id < FEATURE_CNT && rtdata_len < RTFT_DATA_SIZE; ft_id++) {
+ rtft_entry->feature_id = ft_id;
+ rtft_entry->data_len = 0;
+
+ switch (FIELD_GET(FEATURE_TYPE, ft_query->ft_set[ft_id].feature)) {
+ case RTFT_TYPE_NOT_EXIST:
+ fallthrough;
+ case RTFT_TYPE_NOT_SUPPORT:
+ fallthrough;
+ case RTFT_TYPE_MUST_SUPPORT:
+ rtft_entry->support_info = ft_query->ft_set[ft_id];
+ break;
+ case RTFT_TYPE_OPTIONAL_SUPPORT:
+ fallthrough;
+ case RTFT_TYPE_SUPPORT_BACKWARD_COMPAT:
+ rtft_entry->support_info.feature = FEATURE_TYPE_NOT;
+ rtft_entry->support_info.feature |= FEATURE_VER_0;
+ break;
+ }
+
+ rtdata_len += sizeof(*rtft_entry) + le32_to_cpu(rtft_entry->data_len);
+ rtft_entry = (struct runtime_feature_entry *)(feature_data + rtdata_len);
+ }
+ *len = rtdata_len;
+ return 0;
+
+hs_err:
+ *len = 0;
+ return ret;
+}
+
+static int mtk_fsm_send_hs3_msg(struct fsm_hs_info *hs_info)
+{
+ struct mtk_md_fsm *fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
+ unsigned int data_len, msg_size = 0;
+ struct ctrl_msg_header *ctrl_msg_h;
+ struct sk_buff *skb;
+ int ret;
+
+ skb = __dev_alloc_skb(RTFT_DATA_SIZE, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+ memset(skb->data, 0, RTFT_DATA_SIZE);
+
+ msg_size += sizeof(*ctrl_msg_h);
+ ctrl_msg_h = (struct ctrl_msg_header *)skb->data;
+ ctrl_msg_h->id = cpu_to_le32(CTRL_MSG_HS3);
+ ctrl_msg_h->ex_msg = 0;
+ ret = mtk_fsm_append_rtft_entries(fsm->mdev,
+ skb->data + sizeof(*ctrl_msg_h),
+ &data_len, hs_info);
+ if (ret) {
+ dev_kfree_skb(skb);
+ return ret;
+ }
+
+ ctrl_msg_h->data_len = cpu_to_le32(data_len);
+ msg_size += data_len;
+ skb_put(skb, msg_size);
+ ret = mtk_port_internal_write(hs_info->ctrl_port, skb);
+ if (ret <= 0)
+ return ret;
+
+ return 0;
+}
+
+static int mtk_fsm_sap_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
+{
+ struct ctrl_msg_header *ctrl_msg_h;
+ struct mtk_md_fsm *fsm = __fsm;
+ struct fsm_hs_info *hs_info;
+ int ret;
+
+ if (skb->len < sizeof(*ctrl_msg_h)) {
+ dev_kfree_skb(skb);
+ return -EINVAL;
+ }
+
+ ctrl_msg_h = (struct ctrl_msg_header *)skb->data;
+ skb_pull(skb, sizeof(*ctrl_msg_h));
+
+ hs_info = &fsm->hs_info[HS_ID_SAP];
+ if (le32_to_cpu(ctrl_msg_h->id) != CTRL_MSG_HS2) {
+ dev_kfree_skb(skb);
+ return -EPROTO;
+ }
+
+ hs_info->rt_data = skb;
+ hs_info->rt_data_len = skb->len;
+ ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
+ hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
+ if (ret == FSM_EVT_RET_FAIL) {
+ dev_kfree_skb(skb);
+ hs_info->rt_data = NULL;
+ }
+
+ return 0;
+}
+
+static int mtk_fsm_md_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
+{
+ struct ctrl_msg_header *ctrl_msg_h;
+ struct mtk_md_fsm *fsm = __fsm;
+ struct fsm_hs_info *hs_info;
+ bool consumed_skb = false;
+ int ret;
+
+ if (skb->len < sizeof(*ctrl_msg_h)) {
+ dev_kfree_skb(skb);
+ return -EINVAL;
+ }
+
+ ctrl_msg_h = (struct ctrl_msg_header *)skb->data;
+ hs_info = &fsm->hs_info[HS_ID_MD];
+ switch (le32_to_cpu(ctrl_msg_h->id)) {
+ case CTRL_MSG_HS2:
+ skb_pull(skb, sizeof(*ctrl_msg_h));
+ hs_info->rt_data = skb;
+ hs_info->rt_data_len = skb->len;
+ ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
+ hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
+ if (ret != FSM_EVT_RET_FAIL)
+ consumed_skb = true;
+ break;
+ default:
+ dev_err(fsm->mdev->dev, "Invalid ctrl msg id\n");
+ }
+
+ if (!consumed_skb) {
+ dev_kfree_skb(skb);
+ hs_info->rt_data = NULL;
+ }
+
+ return 0;
+}
+
+static int (*ctrl_msg_handler[HS_ID_MAX])(void *__fsm, struct sk_buff *skb) = {
+ [HS_ID_MD] = mtk_fsm_md_ctrl_msg_handler,
+ [HS_ID_SAP] = mtk_fsm_sap_ctrl_msg_handler,
+};
+
+static void mtk_fsm_idle_evt_handler(struct mtk_md_dev *mdev,
+ u32 dev_state, struct mtk_md_fsm *fsm)
+{
+ u32 dev_cfg = dev_state >> DEVICE_CFG_SHIFT & DEVICE_CFG_REGION_MASK;
+ int hs_id;
+
+ if (dev_cfg == DEV_CFG_MD_ONLY)
+ fsm->hs_done_flag = FSM_F_MD_HS_START | FSM_F_MD_HS2_DONE;
+ else
+ fsm->hs_done_flag = FSM_HS_START_MASK | FSM_HS2_DONE_MASK;
+
+ mtk_fsm_evt_submit(mdev, FSM_EVT_STARTUP, FSM_F_DFLT, NULL, 0, 0);
+
+ for (hs_id = 0; hs_id < HS_ID_MAX; hs_id++)
+ mtk_dev_unmask_dev_evt(mdev, fsm->hs_info[hs_id].mhccif_ch);
+}
+
+static int mtk_fsm_early_bootup_handler(u32 status, void *__fsm)
+{
+ struct mtk_md_fsm *fsm = __fsm;
+ struct mtk_md_dev *mdev;
+ u32 dev_state, dev_stage;
+
+ mdev = fsm->mdev;
+ mtk_dev_mask_dev_evt(mdev, status);
+ mtk_dev_clear_dev_evt(mdev, status);
+
+ dev_state = mtk_dev_get_dev_state(mdev);
+ dev_stage = dev_state & REGION_BITMASK;
+ if (dev_stage >= DEV_STAGE_MAX) {
+ dev_err(mdev->dev, "Invalid dev state 0x%x\n", dev_state);
+ return -ENXIO;
+ }
+
+ if (dev_state == fsm->last_dev_state)
+ goto exit;
+ fsm->last_dev_state = dev_state;
+
+ if (dev_stage == DEV_STAGE_IDLE)
+ mtk_fsm_idle_evt_handler(mdev, dev_state, fsm);
+
+exit:
+ return 0;
+}
+
+static int mtk_fsm_ctrl_ch_start(struct mtk_md_fsm *fsm, struct fsm_hs_info *hs_info, int flag)
+{
+ if (!hs_info->ctrl_port) {
+ hs_info->ctrl_port = mtk_port_internal_open(fsm->mdev, hs_info->port_name, flag);
+ if (!hs_info->ctrl_port) {
+ dev_err(fsm->mdev->dev, "Failed to open ctrl port(%s)\n",
+ hs_info->port_name);
+ return -ENODEV;
+ }
+
+ mtk_port_internal_recv_register(hs_info->ctrl_port,
+ ctrl_msg_handler[hs_info->id], fsm);
+ }
+
+ return 0;
+}
+
+static void mtk_fsm_ctrl_ch_stop(struct mtk_md_fsm *fsm)
+{
+ struct fsm_hs_info *hs_info;
+ int hs_id;
+
+ for (hs_id = 0; hs_id < HS_ID_MAX; hs_id++) {
+ hs_info = &fsm->hs_info[hs_id];
+ if (hs_info->ctrl_port) {
+ mtk_port_internal_close(hs_info->ctrl_port);
+ hs_info->ctrl_port = NULL;
+ }
+ }
+}
+
+static void mtk_fsm_switch_state(struct mtk_md_fsm *fsm,
+ enum mtk_fsm_state to_state, struct mtk_fsm_evt *event)
+{
+ char fsm_info[MTK_FSM_INFO_LEN];
+ struct mtk_fsm_notifier *nt;
+ struct mtk_fsm_param param;
+
+ param.from = fsm->state;
+ param.to = to_state;
+ param.evt_id = event ? event->id : FSM_EVT_MAX;
+ param.fsm_flag = event ? event->fsm_flag : FSM_F_DFLT;
+
+ list_for_each_entry(nt, &fsm->pre_notifiers, entry)
+ nt->cb(¶m, nt->data);
+
+ fsm->state = to_state;
+ fsm->fsm_flag |= event ? event->fsm_flag : FSM_F_DFLT;
+
+ snprintf(fsm_info, MTK_FSM_INFO_LEN,
+ "state=%d, fsm_flag=0x%x", to_state, fsm->fsm_flag);
+ mtk_uevent_notify(fsm->mdev->dev, MTK_UEVENT_FSM, fsm_info);
+
+ list_for_each_entry(nt, &fsm->post_notifiers, entry)
+ nt->cb(¶m, nt->data);
+}
+
+static int mtk_fsm_startup_act(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
+{
+ enum mtk_fsm_state to_state = FSM_STATE_BOOTUP;
+ struct fsm_hs_info *hs_info = event->data;
+ struct mtk_md_dev *mdev = fsm->mdev;
+ int ret = 0;
+
+ if (fsm->state != FSM_STATE_ON && fsm->state != FSM_STATE_BOOTUP) {
+ ret = -EPROTO;
+ goto free_rt_data;
+ }
+
+ if (fsm->state != FSM_STATE_BOOTUP) {
+ mtk_fsm_switch_state(fsm, to_state, event);
+ return 0;
+ }
+
+ if (event->fsm_flag & FSM_HS_START_MASK) {
+ mtk_fsm_switch_state(fsm, to_state, event);
+
+ ret = mtk_fsm_ctrl_ch_start(fsm, hs_info, O_NONBLOCK);
+ if (!ret)
+ ret = mtk_fsm_send_hs1_msg(hs_info);
+ if (ret)
+ goto hs_err;
+ } else if (event->fsm_flag & FSM_HS2_DONE_MASK) {
+ ret = mtk_fsm_parse_hs2_msg(hs_info);
+ if (!ret) {
+ mtk_fsm_switch_state(fsm, to_state, event);
+ ret = mtk_fsm_send_hs3_msg(hs_info);
+ }
+ dev_kfree_skb(hs_info->rt_data);
+ hs_info->rt_data = NULL;
+ if (ret)
+ goto hs_err;
+ }
+
+ if (((fsm->fsm_flag | event->fsm_flag) & fsm->hs_done_flag) == fsm->hs_done_flag) {
+ to_state = FSM_STATE_READY;
+ mtk_fsm_switch_state(fsm, to_state, NULL);
+ }
+
+ return 0;
+
+free_rt_data:
+ if (hs_info && hs_info->rt_data) {
+ dev_kfree_skb(hs_info->rt_data);
+ hs_info->rt_data = NULL;
+ }
+hs_err:
+ dev_err(mdev->dev, "Failed to hs with device %d:0x%x, ret=%d",
+ fsm->state, fsm->fsm_flag, ret);
+ return ret;
+}
+
+static void mtk_fsm_evt_release(struct kref *kref)
+{
+ struct mtk_fsm_evt *event = container_of(kref, struct mtk_fsm_evt, kref);
+
+ kfree(event);
+}
+
+static void mtk_fsm_evt_put(struct mtk_fsm_evt *event)
+{
+ kref_put(&event->kref, mtk_fsm_evt_release);
+}
+
+static void mtk_fsm_evt_finish(struct mtk_md_fsm *fsm,
+ struct mtk_fsm_evt *event, int retval)
+{
+ if (event->mode & EVT_MODE_BLOCKING) {
+ event->status = retval;
+ wake_up(&fsm->evt_waitq);
+ }
+ mtk_fsm_evt_put(event);
+}
+
+static void mtk_fsm_evt_cleanup(struct mtk_md_fsm *fsm, struct list_head *evtq)
+{
+ struct mtk_fsm_evt *event, *tmp;
+
+ list_for_each_entry_safe(event, tmp, evtq, entry) {
+ list_del(&event->entry);
+ mtk_fsm_evt_finish(fsm, event, FSM_EVT_RET_FAIL);
+ }
+}
+
+static int mtk_fsm_enter_off_state(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
+{
+ struct mtk_md_dev *mdev = fsm->mdev;
+ int hs_id;
+
+ if (fsm->state == FSM_STATE_OFF || fsm->state == FSM_STATE_INVALID)
+ return -EPROTO;
+
+ mtk_dev_mask_dev_evt(mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC);
+ for (hs_id = 0; hs_id < HS_ID_MAX; hs_id++)
+ mtk_dev_mask_dev_evt(mdev, fsm->hs_info[hs_id].mhccif_ch);
+
+ mtk_fsm_ctrl_ch_stop(fsm);
+ mtk_fsm_switch_state(fsm, FSM_STATE_OFF, event);
+
+ return 0;
+}
+
+static int mtk_fsm_dev_rm_act(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&fsm->evtq_lock, flags);
+ set_bit(EVT_TF_GATECLOSED, &fsm->t_flag);
+ mtk_fsm_evt_cleanup(fsm, &fsm->evtq);
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+
+ return mtk_fsm_enter_off_state(fsm, event);
+}
+
+static int mtk_fsm_hs1_handler(u32 status, void *__hs_info)
+{
+ struct fsm_hs_info *hs_info = __hs_info;
+ struct mtk_md_dev *mdev;
+ struct mtk_md_fsm *fsm;
+
+ fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
+ mdev = fsm->mdev;
+ mtk_fsm_evt_submit(mdev, FSM_EVT_STARTUP,
+ hs_info->fsm_flag_hs1, hs_info, sizeof(*hs_info), 0);
+ mtk_dev_mask_dev_evt(mdev, hs_info->mhccif_ch);
+ mtk_dev_clear_dev_evt(mdev, hs_info->mhccif_ch);
+
+ return 0;
+}
+
+static void mtk_fsm_hs_info_init_by_hsid(struct mtk_md_fsm *fsm, int hs_id)
+{
+ struct fsm_hs_info *hs_info;
+
+ if (hs_id < 0 || hs_id >= HS_ID_MAX) {
+ dev_warn((fsm->mdev)->dev, "hs_id = %d, invalid.\n", hs_id);
+ return;
+ }
+
+ hs_info = &fsm->hs_info[hs_id];
+ hs_info->id = hs_id;
+ hs_info->ctrl_port = NULL;
+ hs_info->rt_data = NULL;
+ switch (hs_id) {
+ case HS_ID_MD:
+ snprintf(hs_info->port_name, PORT_NAME_LEN, "MDCTRL");
+ hs_info->mhccif_ch = DEV_EVT_D2H_ASYNC_HS_NOTIFY_MD;
+ hs_info->fsm_flag_hs1 = FSM_F_MD_HS_START;
+ hs_info->fsm_flag_hs2 = FSM_F_MD_HS2_DONE;
+ hs_info->query_ft_set[QUERY_RTFT_ID_MD_PORT_ENUM].feature =
+ FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_MUST_SUPPORT);
+ hs_info->query_ft_set[QUERY_RTFT_ID_MD_PORT_ENUM].feature |=
+ FIELD_PREP(FEATURE_VER, 0);
+ hs_info->query_ft_set[QUERY_RTFT_ID_MD_PORT_CFG].feature =
+ FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_NOT_SUPPORT);
+ break;
+ case HS_ID_SAP:
+ snprintf(hs_info->port_name, PORT_NAME_LEN, "SAPCTRL");
+ hs_info->mhccif_ch = DEV_EVT_D2H_ASYNC_HS_NOTIFY_SAP;
+ hs_info->fsm_flag_hs1 = FSM_F_SAP_HS_START;
+ hs_info->fsm_flag_hs2 = FSM_F_SAP_HS2_DONE;
+ hs_info->query_ft_set[QUERY_RTFT_ID_SAP_PORT_ENUM].feature =
+ FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_MUST_SUPPORT);
+ hs_info->query_ft_set[QUERY_RTFT_ID_SAP_PORT_ENUM].feature |=
+ FIELD_PREP(FEATURE_VER, 0);
+ break;
+ }
+}
+
+static void mtk_fsm_hs_info_init(struct mtk_md_fsm *fsm)
+{
+ struct mtk_md_dev *mdev = fsm->mdev;
+ struct fsm_hs_info *hs_info;
+ int hs_id;
+
+ for (hs_id = 0; hs_id < HS_ID_MAX; hs_id++) {
+ mtk_fsm_hs_info_init_by_hsid(fsm, hs_id);
+ hs_info = &fsm->hs_info[hs_id];
+ mtk_dev_register_dev_evt(mdev, hs_info->mhccif_ch,
+ mtk_fsm_hs1_handler, hs_info);
+ }
+}
+
+static void mtk_fsm_hs_info_exit(struct mtk_md_fsm *fsm)
+{
+ struct mtk_md_dev *mdev = fsm->mdev;
+ struct fsm_hs_info *hs_info;
+ int hs_id;
+
+ for (hs_id = 0; hs_id < HS_ID_MAX; hs_id++) {
+ hs_info = &fsm->hs_info[hs_id];
+ mtk_dev_unregister_dev_evt(mdev, hs_info->mhccif_ch);
+ }
+}
+
+static int mtk_fsm_dev_add_act(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
+{
+ if (fsm->state != FSM_STATE_OFF && fsm->state != FSM_STATE_INVALID)
+ return -EPROTO;
+
+ mtk_fsm_switch_state(fsm, FSM_STATE_ON, event);
+ mtk_dev_unmask_dev_evt(fsm->mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC);
+
+ return 0;
+}
+
+static int (*evts_act_tbl[FSM_EVT_MAX])(struct mtk_md_fsm *__fsm, struct mtk_fsm_evt *event) = {
+ [FSM_EVT_STARTUP] = mtk_fsm_startup_act,
+ [FSM_EVT_DEV_RM] = mtk_fsm_dev_rm_act,
+ [FSM_EVT_DEV_ADD] = mtk_fsm_dev_add_act,
+};
+
+int mtk_fsm_start(struct mtk_md_dev *mdev)
+{
+ struct mtk_md_fsm *fsm = mdev->fsm;
+
+ if (!fsm)
+ return -EINVAL;
+
+ if (!fsm->fsm_handler)
+ return -EFAULT;
+
+ wake_up_process(fsm->fsm_handler);
+ return 0;
+}
+EXPORT_SYMBOL(mtk_fsm_start);
+
+static void mkt_fsm_notifier_cleanup(struct mtk_md_dev *mdev, struct list_head *ntq)
+{
+ struct mtk_fsm_notifier *nt, *tmp;
+
+ list_for_each_entry_safe(nt, tmp, ntq, entry) {
+ list_del(&nt->entry);
+ dev_warn(mdev->dev, "Having to free notifier(%d) by FSM!\n", nt->id);
+ kfree(nt);
+ }
+}
+
+static void mtk_fsm_notifier_insert(struct mtk_fsm_notifier *notifier, struct list_head *head)
+{
+ struct mtk_fsm_notifier *nt;
+
+ list_for_each_entry(nt, head, entry) {
+ if (notifier->prio > nt->prio) {
+ list_add(¬ifier->entry, nt->entry.prev);
+ return;
+ }
+ }
+ list_add_tail(¬ifier->entry, head);
+}
+
+int mtk_fsm_notifier_register(struct mtk_md_dev *mdev, enum mtk_user_id id,
+ void (*cb)(struct mtk_fsm_param *, void *data),
+ void *data, enum mtk_fsm_prio prio, bool is_pre)
+{
+ struct mtk_md_fsm *fsm = mdev->fsm;
+ struct mtk_fsm_notifier *notifier;
+
+ if (!fsm)
+ return -EINVAL;
+
+ if (id >= MTK_USER_MAX || !cb || prio >= FSM_PRIO_MAX)
+ return -EINVAL;
+
+ notifier = kzalloc_obj(*notifier);
+ if (!notifier)
+ return -ENOMEM;
+
+ INIT_LIST_HEAD(¬ifier->entry);
+ notifier->id = id;
+ notifier->cb = cb;
+ notifier->data = data;
+ notifier->prio = prio;
+
+ if (is_pre)
+ mtk_fsm_notifier_insert(notifier, &fsm->pre_notifiers);
+ else
+ mtk_fsm_notifier_insert(notifier, &fsm->post_notifiers);
+
+ return 0;
+}
+EXPORT_SYMBOL(mtk_fsm_notifier_register);
+
+int mtk_fsm_notifier_unregister(struct mtk_md_dev *mdev, enum mtk_user_id id)
+{
+ struct mtk_md_fsm *fsm = mdev->fsm;
+ struct mtk_fsm_notifier *nt, *tmp;
+
+ if (!fsm)
+ return -EINVAL;
+
+ list_for_each_entry_safe(nt, tmp, &fsm->pre_notifiers, entry) {
+ if (nt->id == id) {
+ list_del(&nt->entry);
+ kfree(nt);
+ break;
+ }
+ }
+ list_for_each_entry_safe(nt, tmp, &fsm->post_notifiers, entry) {
+ if (nt->id == id) {
+ list_del(&nt->entry);
+ kfree(nt);
+ break;
+ }
+ }
+ return 0;
+}
+EXPORT_SYMBOL(mtk_fsm_notifier_unregister);
+
+int mtk_fsm_evt_submit(struct mtk_md_dev *mdev,
+ enum mtk_fsm_evt_id id, enum mtk_fsm_flag flag,
+ void *data, unsigned int len, unsigned char mode)
+{
+ struct mtk_md_fsm *fsm = mdev->fsm;
+ struct mtk_fsm_evt *event;
+ unsigned long flags;
+ int ret = 0;
+
+ if (!fsm || id >= FSM_EVT_MAX) {
+ dev_err(mdev->dev, "Invalid param!\n");
+ return FSM_EVT_RET_FAIL;
+ }
+
+ if (test_bit(EVT_TF_GATECLOSED, &fsm->t_flag)) {
+ dev_err(mdev->dev, "Failed to submit evt, fsm has been removed!\n");
+ return FSM_EVT_RET_FAIL;
+ }
+
+ event = kzalloc(sizeof(*event),
+ (in_hardirq() || in_softirq() || irqs_disabled()) ?
+ GFP_ATOMIC : GFP_KERNEL);
+ if (!event)
+ return FSM_EVT_RET_FAIL;
+
+ kref_init(&event->kref);
+ event->mdev = mdev;
+ event->id = id;
+ event->fsm_flag = flag;
+ event->status = FSM_EVT_RET_ONGOING;
+ event->data = data;
+ event->len = len;
+ event->mode = mode;
+
+ spin_lock_irqsave(&fsm->evtq_lock, flags);
+ if (test_bit(EVT_TF_GATECLOSED, &fsm->t_flag)) {
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+ mtk_fsm_evt_put(event);
+ dev_err(mdev->dev, "Failed to add event, fsm dev has been removed!\n");
+ return FSM_EVT_RET_FAIL;
+ }
+
+ kref_get(&event->kref);
+ if (mode & EVT_MODE_TOHEAD)
+ list_add(&event->entry, &fsm->evtq);
+ else
+ list_add_tail(&event->entry, &fsm->evtq);
+ wake_up_process(fsm->fsm_handler);
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+
+ if (mode & EVT_MODE_BLOCKING) {
+ ret = wait_event_timeout(fsm->evt_waitq,
+ (event->status != 0), BLOCKING_EVT_TIMEOUT);
+ if (!ret && event->status != FSM_EVT_RET_DONE) {
+ dev_err(mdev->dev, "Handling fsm blocking event timeout!\n");
+ ret = -ETIMEDOUT;
+ } else {
+ ret = event->status;
+ }
+ }
+ mtk_fsm_evt_put(event);
+
+ return ret;
+}
+EXPORT_SYMBOL(mtk_fsm_evt_submit);
+
+static int mtk_fsm_evt_handler(void *__fsm)
+{
+ struct mtk_md_fsm *fsm = __fsm;
+ struct mtk_fsm_evt *event;
+ unsigned long flags;
+ int ret;
+
+wake_up:
+ set_current_state(TASK_UNINTERRUPTIBLE);
+ while (!kthread_should_stop() && !list_empty(&fsm->evtq)) {
+ set_current_state(TASK_RUNNING);
+ spin_lock_irqsave(&fsm->evtq_lock, flags);
+ event = list_first_entry(&fsm->evtq, struct mtk_fsm_evt, entry);
+ list_del(&event->entry);
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+
+ if (event->id < FSM_EVT_MAX) {
+ ret = evts_act_tbl[event->id](fsm, event);
+ if (ret) {
+ dev_err((fsm->mdev)->dev,
+ "Failed to handle evt, fsm state = %d, ret = %d\n",
+ fsm->state, ret);
+ mtk_fsm_evt_finish(fsm, event, FSM_EVT_RET_FAIL);
+ } else {
+ mtk_fsm_evt_finish(fsm, event, FSM_EVT_RET_DONE);
+ }
+ } else {
+ mtk_fsm_evt_finish(fsm, event, FSM_EVT_RET_DONE);
+ }
+ }
+
+ if (kthread_should_stop()) {
+ set_current_state(TASK_RUNNING);
+ return 0;
+ }
+
+ schedule();
+ goto wake_up;
+}
+
+int mtk_fsm_init(struct mtk_md_dev *mdev)
+{
+ struct mtk_md_fsm *fsm;
+ int ret;
+
+ fsm = devm_kzalloc(mdev->dev, sizeof(*fsm), GFP_KERNEL);
+ if (!fsm)
+ return -ENOMEM;
+
+ fsm->fsm_handler = kthread_create(mtk_fsm_evt_handler, fsm, "fsm_evt_thread%d_%s",
+ mdev->hw_ver, mdev->dev_str);
+ if (IS_ERR(fsm->fsm_handler)) {
+ ret = PTR_ERR(fsm->fsm_handler);
+ goto exit;
+ }
+
+ fsm->mdev = mdev;
+ fsm->state = FSM_STATE_INVALID;
+ fsm->fsm_flag = FSM_F_DFLT;
+
+ INIT_LIST_HEAD(&fsm->evtq);
+ spin_lock_init(&fsm->evtq_lock);
+ init_waitqueue_head(&fsm->evt_waitq);
+
+ INIT_LIST_HEAD(&fsm->pre_notifiers);
+ INIT_LIST_HEAD(&fsm->post_notifiers);
+
+ mtk_fsm_hs_info_init(fsm);
+ mtk_dev_register_dev_evt(mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC,
+ mtk_fsm_early_bootup_handler, fsm);
+ mdev->fsm = fsm;
+ return 0;
+exit:
+ return ret;
+}
+EXPORT_SYMBOL(mtk_fsm_init);
+
+int mtk_fsm_exit(struct mtk_md_dev *mdev)
+{
+ struct mtk_md_fsm *fsm = mdev->fsm;
+ struct task_struct *handler;
+ unsigned long flags;
+
+ if (!fsm)
+ return -EINVAL;
+
+ spin_lock_irqsave(&fsm->evtq_lock, flags);
+ handler = fsm->fsm_handler;
+ fsm->fsm_handler = NULL;
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+
+ if (handler)
+ kthread_stop(handler);
+
+ spin_lock_irqsave(&fsm->evtq_lock, flags);
+ if (WARN_ON(!list_empty(&fsm->evtq)))
+ mtk_fsm_evt_cleanup(fsm, &fsm->evtq);
+ spin_unlock_irqrestore(&fsm->evtq_lock, flags);
+
+ for (int i = 0; i < HS_ID_MAX; i++) {
+ if (fsm->hs_info[i].rt_data) {
+ dev_kfree_skb(fsm->hs_info[i].rt_data);
+ fsm->hs_info[i].rt_data = NULL;
+ }
+ }
+
+ mkt_fsm_notifier_cleanup(mdev, &fsm->pre_notifiers);
+ mkt_fsm_notifier_cleanup(mdev, &fsm->post_notifiers);
+
+ mtk_dev_unregister_dev_evt(mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC);
+ mtk_fsm_hs_info_exit(fsm);
+
+ return 0;
+}
+EXPORT_SYMBOL(mtk_fsm_exit);
diff --git a/drivers/net/wwan/t9xx/mtk_fsm.h b/drivers/net/wwan/t9xx/mtk_fsm.h
new file mode 100644
index 000000000000..f2fc66bcef61
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_fsm.h
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_FSM_H__
+#define __MTK_FSM_H__
+
+#include "mtk_dev.h"
+
+#define FEATURE_CNT (64)
+#define FEATURE_QUERY_PATTERN (0x49434343)
+
+#define FEATURE_TYPE GENMASK(3, 0)
+#define FEATURE_VER GENMASK(7, 4)
+
+#define FEATURE_TYPE_NOT FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_NOT_SUPPORT)
+#define FEATURE_TYPE_MUST FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_MUST_SUPPORT)
+#define FEATURE_TYPE_OPTIONAL FIELD_PREP(FEATURE_TYPE, RTFT_TYPE_OPTIONAL_SUPPORT)
+#define FEATURE_VER_0 FIELD_PREP(FEATURE_VER, 0)
+
+#define EVT_MODE_BLOCKING (0x01)
+#define EVT_MODE_TOHEAD (0x02)
+
+#define FSM_EVT_RET_FAIL (-1)
+#define FSM_EVT_RET_ONGOING (0)
+#define FSM_EVT_RET_DONE (1)
+
+enum mtk_fsm_flag {
+ FSM_F_DFLT = 0,
+ FSM_F_SAP_HS_START = BIT(0),
+ FSM_F_SAP_HS2_DONE = BIT(1),
+ FSM_F_MD_HS_START = BIT(2),
+ FSM_F_MD_HS2_DONE = BIT(3),
+};
+
+enum mtk_fsm_state {
+ FSM_STATE_INVALID = 0,
+ FSM_STATE_OFF,
+ FSM_STATE_ON,
+ FSM_STATE_BOOTUP,
+ FSM_STATE_READY,
+};
+
+enum mtk_fsm_evt_id {
+ FSM_EVT_STARTUP = 0,
+ FSM_EVT_DEV_RM,
+ FSM_EVT_DEV_ADD,
+ FSM_EVT_MAX
+};
+
+enum mtk_fsm_prio {
+ FSM_PRIO_0 = 0,
+ FSM_PRIO_1 = 1,
+ FSM_PRIO_MAX
+};
+
+struct mtk_fsm_param {
+ enum mtk_fsm_state from;
+ enum mtk_fsm_state to;
+ enum mtk_fsm_evt_id evt_id;
+ enum mtk_fsm_flag fsm_flag;
+};
+
+#define PORT_NAME_LEN 20
+
+enum handshake_info_id {
+ HS_ID_MD = 0,
+ HS_ID_SAP,
+ HS_ID_MAX
+};
+
+struct runtime_feature_info {
+ u8 feature;
+};
+
+struct fsm_hs_info {
+ unsigned char id;
+ void *ctrl_port;
+ char port_name[PORT_NAME_LEN];
+ unsigned int mhccif_ch;
+ unsigned int fsm_flag_hs1;
+ unsigned int fsm_flag_hs2;
+ /* the feature that the device should support */
+ struct runtime_feature_info query_ft_set[FEATURE_CNT];
+ /* runtime data from device need to be parsed by host */
+ void *rt_data;
+ unsigned int rt_data_len;
+};
+
+struct mtk_md_fsm {
+ struct mtk_md_dev *mdev;
+ struct task_struct *fsm_handler;
+ struct fsm_hs_info hs_info[HS_ID_MAX];
+ unsigned int hs_done_flag;
+ unsigned long t_flag;
+ u32 last_dev_state;
+ enum mtk_fsm_state state;
+ unsigned int fsm_flag;
+ struct list_head evtq;
+ /* protect evtq */
+ spinlock_t evtq_lock;
+ /* waitq for fsm blocking submit */
+ wait_queue_head_t evt_waitq;
+ struct list_head pre_notifiers;
+ struct list_head post_notifiers;
+};
+
+struct mtk_fsm_evt {
+ struct list_head entry;
+ struct kref kref;
+ struct mtk_md_dev *mdev;
+ enum mtk_fsm_evt_id id;
+ unsigned int fsm_flag;
+ int status;
+ unsigned char mode;
+ unsigned int len;
+ void *data;
+};
+
+struct mtk_fsm_notifier {
+ struct list_head entry;
+ enum mtk_user_id id;
+ void (*cb)(struct mtk_fsm_param *param, void *data);
+ void *data;
+ enum mtk_fsm_prio prio;
+};
+
+int mtk_fsm_init(struct mtk_md_dev *mdev);
+int mtk_fsm_exit(struct mtk_md_dev *mdev);
+int mtk_fsm_start(struct mtk_md_dev *mdev);
+int mtk_fsm_notifier_register(struct mtk_md_dev *mdev, enum mtk_user_id id,
+ void (*cb)(struct mtk_fsm_param *, void *data),
+ void *data, enum mtk_fsm_prio prio, bool is_pre);
+int mtk_fsm_notifier_unregister(struct mtk_md_dev *mdev, enum mtk_user_id id);
+int mtk_fsm_evt_submit(struct mtk_md_dev *mdev,
+ enum mtk_fsm_evt_id id, enum mtk_fsm_flag flag,
+ void *data, unsigned int len, unsigned char mode);
+
+#endif /* __MTK_FSM_H__ */
diff --git a/drivers/net/wwan/t9xx/mtk_port.c b/drivers/net/wwan/t9xx/mtk_port.c
index a71c097c6d39..ee52b44ddf86 100644
--- a/drivers/net/wwan/t9xx/mtk_port.c
+++ b/drivers/net/wwan/t9xx/mtk_port.c
@@ -819,6 +819,69 @@ int mtk_port_ch_disable(struct mtk_port *port)
return ret;
}
+static void mtk_port_disable(struct mtk_port_mngr *port_mngr)
+{
+ struct radix_tree_iter iter;
+ struct mtk_port *port;
+ void __rcu **slot;
+ int tbl_type;
+
+ tbl_type = PORT_TBL_SAP;
+ do {
+ radix_tree_for_each_slot(slot, &port_mngr->port_tbl[tbl_type],
+ &iter, 0) {
+ MTK_PORT_SEARCH_FROM_RADIX_TREE(port, slot);
+ MTK_PORT_INTERNAL_NODE_CHECK(port, slot, iter);
+ ports_ops[port->info.type]->disable(port);
+ }
+ } while (++tbl_type < PORT_TBL_MAX);
+}
+
+void mtk_port_mngr_fsm_state_handler(struct mtk_fsm_param *fsm_param, void *arg)
+{
+ struct mtk_port_mngr *port_mngr;
+
+ if (!fsm_param || !arg)
+ return;
+
+ port_mngr = arg;
+
+ switch (fsm_param->to) {
+ case FSM_STATE_OFF:
+ mtk_port_disable(port_mngr);
+ break;
+ default:
+ break;
+ }
+}
+
+void mtk_port_mngr_fsm_state_handler_late(struct mtk_fsm_param *fsm_param, void *arg)
+{
+ struct mtk_port_mngr *port_mngr;
+ struct mtk_port *port;
+
+ if (!fsm_param || !arg)
+ return;
+
+ port_mngr = arg;
+
+ switch (fsm_param->to) {
+ case FSM_STATE_BOOTUP:
+ if (fsm_param->fsm_flag & FSM_F_MD_HS_START) {
+ port = mtk_port_search_by_id(port_mngr, CCCI_CONTROL_RX);
+ if (port)
+ ports_ops[port->info.type]->enable(port);
+ } else if (fsm_param->fsm_flag & FSM_F_SAP_HS_START) {
+ port = mtk_port_search_by_id(port_mngr, CCCI_SAP_CONTROL_RX);
+ if (port)
+ ports_ops[port->info.type]->enable(port);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
int mtk_port_mngr_init(struct mtk_ctrl_blk *ctrl_blk, struct mtk_port_cfg *port_cfg, int port_cnt)
{
struct mtk_port_mngr *port_mngr;
diff --git a/drivers/net/wwan/t9xx/mtk_port.h b/drivers/net/wwan/t9xx/mtk_port.h
index 0a42313e348f..ba69eb37c7f9 100644
--- a/drivers/net/wwan/t9xx/mtk_port.h
+++ b/drivers/net/wwan/t9xx/mtk_port.h
@@ -152,6 +152,8 @@ int mtk_port_send_data(struct mtk_port *port, void *data);
int mtk_port_status_update(struct mtk_md_dev *mdev, void *data, u32 data_len);
int mtk_port_ch_enable(struct mtk_port *port);
int mtk_port_ch_disable(struct mtk_port *port);
+void mtk_port_mngr_fsm_state_handler(struct mtk_fsm_param *fsm_param, void *arg);
+void mtk_port_mngr_fsm_state_handler_late(struct mtk_fsm_param *fsm_param, void *arg);
int mtk_port_mngr_init(struct mtk_ctrl_blk *ctrl_blk, struct mtk_port_cfg *port_cfg, int port_cnt);
void mtk_port_mngr_exit(struct mtk_ctrl_blk *ctrl_blk);
void mtk_port_trb_init(struct mtk_port *port, struct trb *trb, enum mtk_trb_cmd_type cmd,
diff --git a/drivers/net/wwan/t9xx/mtk_utility.h b/drivers/net/wwan/t9xx/mtk_utility.h
new file mode 100644
index 000000000000..b72db3842d2d
--- /dev/null
+++ b/drivers/net/wwan/t9xx/mtk_utility.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Copyright (c) 2022, MediaTek Inc.
+ */
+
+#ifndef __MTK_UTILITY_H__
+#define __MTK_UTILITY_H__
+
+#include <linux/device.h>
+#include "mtk_dev.h"
+
+#define MTK_UEVENT_INFO_LEN 128
+
+/* MTK uevent */
+enum mtk_uevent_id {
+ MTK_UEVENT_UNDEF = 0,
+ MTK_UEVENT_FSM = 1,
+ MTK_UEVENT_MINIDUMP = 2,
+ MTK_UEVENT_LOWPOWER = 3,
+ MTK_UEVENT_MAX
+};
+
+static inline void mtk_uevent_notify(struct device *dev, enum mtk_uevent_id id, const char *info)
+{
+ char buf[MTK_UEVENT_INFO_LEN];
+ char *ext[2] = {NULL, NULL};
+
+ snprintf(buf, MTK_UEVENT_INFO_LEN, "%s:event_id=%d, info=%s",
+ dev->kobj.name, id, info);
+ ext[0] = buf;
+ kobject_uevent_env(&dev->kobj, KOBJ_CHANGE, ext);
+}
+#endif /* __MTK_UTILITY_H__ */
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
index 097b20d8fbd3..1431c2797eb5 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
@@ -34,12 +34,172 @@
#define CLDMA_RETRY_DELAY_MS (100)
#define NO_BUDGET (0)
+static struct cldma_drv_info_desc cldma_drv_info_tbl[] = {
+ {0x01CA, &drv_ops_name(m9xx), &cldma_regs_name(m9xx)},
+ {0, NULL},
+};
+
+static void mtk_cldma_get_drv_info(struct cldma_drv_info *drv_info, u32 hw_ver)
+{
+ struct cldma_drv_info_desc *p_drv_info;
+ u8 i;
+
+ for (i = 0; (p_drv_info = &cldma_drv_info_tbl[i]) && p_drv_info &&
+ p_drv_info->drv_ops && p_drv_info->hw_regs; i++)
+ if (p_drv_info->hw_ver == hw_ver) {
+ drv_info->drv_ops = p_drv_info->drv_ops;
+ drv_info->hw_regs = p_drv_info->hw_regs;
+ }
+}
+
+static int mtk_cldma_isr(int irq_id, void *param)
+{
+ struct cldma_drv_info *drv_info = param;
+ struct mtk_md_dev *mdev;
+ u32 tx_done, rx_done;
+ u32 tx_sta, rx_sta;
+ struct txq *txq;
+ struct rxq *rxq;
+ int i;
+
+ mdev = drv_info->mdev;
+ drv_info->drv_ops->cldma_get_intr_status(drv_info, &tx_sta, &rx_sta);
+ tx_done = (tx_sta >> QUEUE_XFER_DONE) & 0xFF;
+ rx_done = (rx_sta >> QUEUE_XFER_DONE) & 0xFF;
+
+ if (tx_done) {
+ for (i = 0; i < HW_QUEUE_NUM; i++) {
+ txq = drv_info->txq[i];
+ if (!(tx_done & BIT(i)) || !txq)
+ continue;
+ queue_work(drv_info->wq, &txq->tx_done_work);
+ }
+ }
+ if (rx_done) {
+ for (i = 0; i < HW_QUEUE_NUM; i++) {
+ rxq = drv_info->rxq[i];
+ if (!(rx_done & BIT(i)) || !rxq)
+ continue;
+ queue_work(drv_info->wq, &rxq->rx_done_work);
+ }
+ }
+
+ mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
+ mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
+
+ return IRQ_HANDLED;
+}
+
static const int mtk_cldma_hw_id_tbl[NR_CLDMA] = {
[CLDMA0] = CLDMA0_HW_ID,
[CLDMA1] = CLDMA1_HW_ID,
- [CLDMA4] = CLDMA4_HW_ID,
};
+static int mtk_cldma_dev_init(struct cldma_dev *cd, int hif_id)
+{
+ char gpd_pool_name[DMA_POOL_NAME_LEN];
+ char bd_pool_name[DMA_POOL_NAME_LEN];
+ struct cldma_drv_info *drv_info;
+ struct cldma_hw_regs *hw_regs;
+ struct mtk_md_dev *mdev;
+ unsigned int flag;
+ int hw_id, ret;
+
+ if (!cd || hif_id >= NR_CLDMA)
+ return -EINVAL;
+
+ if (cd->cldma_drv_info[hif_id])
+ return 0;
+
+ hw_id = mtk_cldma_hw_id_tbl[hif_id];
+ mdev = cd->trans->mdev;
+ drv_info = kzalloc_obj(*drv_info);
+ if (!drv_info)
+ return -ENOMEM;
+
+ drv_info->cd = cd;
+ drv_info->mdev = mdev;
+ drv_info->hif_id = hif_id;
+ drv_info->hw_id = hw_id;
+ mtk_cldma_get_drv_info(drv_info, mdev->hw_ver);
+
+ if (!drv_info->drv_ops || !drv_info->hw_regs) {
+ dev_err(mdev->dev, "Failed to find CLDMA Driver for PCI %x\n", mdev->hw_ver);
+ ret = -EIO;
+ goto err_free_drv_info;
+ }
+
+ hw_regs = drv_info->hw_regs;
+ snprintf(gpd_pool_name, DMA_POOL_NAME_LEN, "cldma%d_gpd_pool_%s",
+ hw_id, mdev->dev_str);
+ snprintf(bd_pool_name, DMA_POOL_NAME_LEN, "cldma%d_bd_pool_%s",
+ hw_id, mdev->dev_str);
+ drv_info->gpd_dma_pool = dma_pool_create(gpd_pool_name, mdev->dev,
+ sizeof(union gpd), 4, 0);
+ if (!drv_info->gpd_dma_pool) {
+ dev_err(mdev->dev, "Failed to alloc gpd dma pool for cldma%d\n", hw_id);
+ ret = -ENOMEM;
+ goto err_free_drv_info;
+ }
+ drv_info->bd_dma_pool = dma_pool_create(bd_pool_name, mdev->dev,
+ sizeof(union bd), 4, 0);
+ if (!drv_info->bd_dma_pool) {
+ dev_err(mdev->dev, "Failed to alloc bd dma pool for cldma%d\n", hw_id);
+ ret = -ENOMEM;
+ goto err_destroy_gpd_pool;
+ }
+
+ switch (hif_id) {
+ case CLDMA0:
+ drv_info->pci_ext_irq_id = mtk_pci_get_irq_id(mdev, MTK_IRQ_SRC_CLDMA0);
+ drv_info->base_addr = hw_regs->cldma0_base_addr;
+ break;
+ case CLDMA1:
+ drv_info->pci_ext_irq_id = mtk_pci_get_irq_id(mdev, MTK_IRQ_SRC_CLDMA1);
+ drv_info->base_addr = hw_regs->cldma1_base_addr;
+ break;
+ default:
+ ret = -EINVAL;
+ goto err_destroy_dma_pool;
+ }
+
+ flag = WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_HIGHPRI;
+ drv_info->wq = alloc_workqueue("cldma%d_workq_%s", flag, 0, hw_id, mdev->dev_str);
+ if (!drv_info->wq) {
+ dev_err(mdev->dev, "Failed to alloc work queue for cldma%d\n", hw_id);
+ ret = -ENOMEM;
+ goto err_destroy_dma_pool;
+ }
+
+ drv_info->drv_ops->cldma_drv_init(drv_info);
+
+ /* mask/clear PCI CLDMA L1 interrupt */
+ mtk_pci_mask_irq(mdev, drv_info->pci_ext_irq_id);
+ mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
+
+ /* register CLDMA interrupt handler */
+ ret = mtk_pci_register_irq(mdev, drv_info->pci_ext_irq_id, mtk_cldma_isr, drv_info);
+ if (ret)
+ goto err_destroy_wq;
+
+ /* unmask PCI CLDMA L1 interrupt */
+ mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
+
+ cd->cldma_drv_info[hif_id] = drv_info;
+ return 0;
+
+err_destroy_wq:
+ destroy_workqueue(drv_info->wq);
+err_destroy_dma_pool:
+ dma_pool_destroy(drv_info->bd_dma_pool);
+err_destroy_gpd_pool:
+ dma_pool_destroy(drv_info->gpd_dma_pool);
+err_free_drv_info:
+ kfree(drv_info);
+
+ return ret;
+}
+
static void mtk_cldma_clr_bd_dsc(struct cldma_drv_info *drv_info,
struct bd_dsc *bd_dsc_pool, int nr_bds)
{
@@ -857,6 +1017,7 @@ static void mtk_cldma_rxq_free(struct cldma_drv_info *drv_info, u32 rxqno)
if (req->skb) {
if (rxq->nr_bds) {
skb_shinfo(req->skb)->frag_list = NULL;
+ dev_kfree_skb_any(req->skb);
} else {
if (req->data_dma_addr)
dma_unmap_single(mdev->dev, req->data_dma_addr,
@@ -885,6 +1046,44 @@ static void mtk_cldma_rxq_free(struct cldma_drv_info *drv_info, u32 rxqno)
kfree(rxq);
}
+static int mtk_cldma_dev_exit(struct cldma_dev *cd, int hif_id)
+{
+ struct cldma_drv_info *drv_info;
+ struct mtk_md_dev *mdev;
+ int virq_id;
+ int i;
+
+ if (!cd || hif_id >= NR_CLDMA)
+ return -EINVAL;
+
+ if (!cd->cldma_drv_info[hif_id])
+ return 0;
+
+ /* free cldma descriptor */
+ drv_info = cd->cldma_drv_info[hif_id];
+ mdev = cd->trans->mdev;
+ virq_id = mtk_pci_get_virq_id(mdev, drv_info->pci_ext_irq_id);
+ mtk_pci_mask_irq(mdev, drv_info->pci_ext_irq_id);
+ synchronize_irq(virq_id);
+ for (i = 0; i < HW_QUEUE_NUM; i++) {
+ if (drv_info->txq[i])
+ mtk_cldma_txq_free(drv_info, drv_info->txq[i]->txqno);
+ if (drv_info->rxq[i])
+ mtk_cldma_rxq_free(drv_info, drv_info->rxq[i]->rxqno);
+ }
+
+ flush_workqueue(drv_info->wq);
+ destroy_workqueue(drv_info->wq);
+ dma_pool_destroy(drv_info->bd_dma_pool);
+ dma_pool_destroy(drv_info->gpd_dma_pool);
+ mtk_pci_unregister_irq(mdev, drv_info->pci_ext_irq_id);
+
+ kfree(drv_info);
+ cd->cldma_drv_info[hif_id] = NULL;
+
+ return 0;
+}
+
static int mtk_cldma_start_xfer(struct cldma_drv_info *drv_info, u32 qno)
{
struct cldma_drv_ops *drv_ops;
@@ -1195,6 +1394,27 @@ int mtk_cldma_trb_process(void *dev, struct sk_buff *skb)
return trb_act_tbl[trb->cmd](cd, skb);
}
+void mtk_cldma_fsm_state_listener(struct mtk_fsm_param *param, struct mtk_ctrl_trans *trans)
+{
+ struct cldma_dev *cd = trans->dev;
+ int i;
+
+ switch (param->to) {
+ case FSM_STATE_BOOTUP:
+ if (param->fsm_flag & FSM_F_SAP_HS_START)
+ mtk_cldma_dev_init(cd, CLDMA0);
+ else if (param->fsm_flag & FSM_F_MD_HS_START)
+ mtk_cldma_dev_init(cd, CLDMA1);
+ break;
+ case FSM_STATE_OFF:
+ for (i = 0; i < NR_CLDMA; i++)
+ mtk_cldma_dev_exit(cd, i);
+ break;
+ default:
+ break;
+ }
+}
+
int mtk_cldma_check_ch_cfg(void *dev, struct queue_info *que)
{
struct cldma_drv_info *drv_info;
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h
index 8763c23abf54..6de87b7ffd45 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.h
@@ -11,7 +11,6 @@
#define LINK_ERROR_VAL (0xFFFFFFFF)
#define CLDMA0_HW_ID (0)
#define CLDMA1_HW_ID (1)
-#define CLDMA4_HW_ID (4)
struct cldma_hw_regs {
u8 cldma_rx_skb_pool_max_size;
@@ -36,7 +35,6 @@ struct cldma_hw_regs {
u16 reg_cldma_l2rimsr0;
u16 reg_cldma_l2rimsr1;
u16 reg_cldma_int_mask;
- u16 reg_cldma4_int_mask;
u16 reg_cldma_slp_mem_ctl;
u16 reg_cldma_busy_mask;
u16 reg_cldma_ip_busy_to_pcie_mask;
@@ -58,7 +56,6 @@ struct cldma_hw_regs {
u32 rq_err_int_bitmask;
u32 cldma0_base_addr;
u32 cldma1_base_addr;
- u32 cldma4_base_addr;
u32 rq_active_start_err_int_bitmask;
u32 reg_cldma_ul_start_addrl_0;
u32 reg_cldma_ul_start_addrh_0;
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c
index d9145d146a5c..a59c35fc1577 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.c
@@ -34,7 +34,6 @@
struct cldma_hw_regs mtk_cldma_regs_m9xx = {
.cldma0_base_addr = CLDMA0_BASE_ADDR,
.cldma1_base_addr = CLDMA1_BASE_ADDR,
- .cldma4_base_addr = CLDMA4_BASE_ADDR,
.cldma_rx_skb_pool_max_size = CLDMA_RX_SKB_POOL_MAX_SIZE,
.cldma_rx_skb_reload_threshold = CLDMA_RX_SKB_RELOAD_THRESHOLD,
.tq_err_int_offset = TQ_ERR_INT_OFFSET,
@@ -93,7 +92,6 @@ struct cldma_hw_regs mtk_cldma_regs_m9xx = {
.reg_cldma_l3risar1 = REG_CLDMA_L3RISAR1,
.reg_cldma_ip_busy = REG_CLDMA_IP_BUSY,
.reg_cldma_int_mask = REG_CLDMA_INT_EAP_USIP_MASK,
- .reg_cldma4_int_mask = REG_CLDMA_INT_WF_MASK,
.reg_cldma_ip_busy_to_pcie_mask = REG_CLDMA_IP_BUSY_TO_PCIE_MASK,
.reg_cldma_ip_busy_to_pcie_mask_set = REG_CLDMA_IP_BUSY_TO_PCIE_MASK_SET,
.reg_cldma_ip_busy_to_pcie_mask_clr = REG_CLDMA_IP_BUSY_TO_PCIE_MASK_CLR,
@@ -135,10 +133,7 @@ static void mtk_cldma_drv_init_m9xx(struct cldma_drv_info *drv_info)
ALLQ << 24);
/* enable interrupt to PCIe */
- if (drv_info->hw_id == CLDMA4_HW_ID)
- mtk_pci_write32(mdev, base + hw_regs->reg_cldma4_int_mask, 0);
- else
- mtk_pci_write32(mdev, base + hw_regs->reg_cldma_int_mask, 0);
+ mtk_pci_write32(mdev, base + hw_regs->reg_cldma_int_mask, 0);
/* disable illegal memory check */
mtk_pci_write32(mdev, base + hw_regs->reg_cldma_ul_dummy_0, 1);
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h
index 2c63c43ff065..f113c4c1068a 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma_drv_m9xx.h
@@ -8,7 +8,6 @@
#define CLDMA0_BASE_ADDR (0x1021C000)
#define CLDMA1_BASE_ADDR (0x1021E000)
-#define CLDMA4_BASE_ADDR (0x10224000)
#define CLDMA_RX_SKB_POOL_MAX_SIZE (64)
#define CLDMA_RX_SKB_RELOAD_THRESHOLD (16)
@@ -80,7 +79,6 @@
#define REG_CLDMA_L2RIMSR1 (0x0800 + 0x00FC)
#define REG_CLDMA_INT_EAP_USIP_MASK (0x0800 + 0x011C)
-#define REG_CLDMA_INT_WF_MASK (0x0800 + 0x0120)
#define REG_CLDMA_RQ1_GPD_DONE_CNT (0x0800 + 0x0174)
#define REG_CLDMA_TQ1_GPD_DONE_CNT (0x0800 + 0x0184)
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
index dbc17da590bc..bf4e110b485c 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
@@ -883,22 +883,34 @@ static int mtk_pci_dev_init(struct mtk_md_dev *mdev)
{
int ret;
- ret = mtk_trans_ctrl_init(mdev);
+ ret = mtk_fsm_init(mdev);
if (ret) {
- dev_err(mdev->dev, "Failed to initialize control plane: %d\n", ret);
+ dev_err(mdev->dev, "Failed to initialize FSM: %d\n", ret);
return ret;
}
+ ret = mtk_trans_ctrl_init(mdev);
+ if (ret)
+ goto free_fsm;
+
return 0;
+free_fsm:
+ mtk_fsm_exit(mdev);
+ return ret;
}
static void mtk_pci_dev_exit(struct mtk_md_dev *mdev)
{
+ mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_RM, 0, NULL, 0,
+ EVT_MODE_BLOCKING | EVT_MODE_TOHEAD);
mtk_trans_ctrl_exit(mdev);
+ mtk_fsm_exit(mdev);
}
static int mtk_pci_dev_start(struct mtk_md_dev *mdev)
{
+ mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_ADD, 0, NULL, 0, 0);
+ mtk_fsm_start(mdev);
return 0;
}
static const struct mtk_dev_ops pci_hw_ops = {
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
index 6a0a415a556e..fa36a94014ea 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
@@ -455,6 +455,15 @@ static int mtk_pcie_hif_submit_skb(struct mtk_md_dev *mdev, struct sk_buff *skb,
return 0;
}
+static void mtk_pcie_hif_fsm_indication(struct mtk_md_dev *mdev, struct mtk_fsm_param *param)
+{
+ struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
+ struct mtk_ctrl_trans *trans;
+
+ trans = ctrl_blk->ctrl_hw_priv;
+ mtk_cldma_fsm_state_listener(param, trans);
+}
+
static int mtk_pcie_hif_cmd_func(struct mtk_md_dev *mdev, int cmd, void *data)
{
struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
@@ -482,6 +491,7 @@ static struct mtk_ctrl_hif_ops pcie_ctrl_ops = {
.init = mtk_pcie_hif_init,
.exit = mtk_pcie_hif_exit,
.submit_skb = mtk_pcie_hif_submit_skb,
+ .fsm_indication = mtk_pcie_hif_fsm_indication,
.send_cmd = mtk_pcie_hif_cmd_func,
};
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
index a3ff56ddf86f..4b9c9db6ad71 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
+++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
@@ -29,7 +29,6 @@
enum mtk_hif_id {
CLDMA0,
CLDMA1,
- CLDMA4,
NR_CLDMA
};
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
` (4 preceding siblings ...)
2026-08-11 7:14 ` [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry Jack Wu via B4 Relay
2026-08-17 22:39 ` [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jakub Kicinski
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
Add AT & MBIM ports to the port infrastructure.
The WWAN initialization method is responsible for creating the
corresponding ports using the WWAN framework infrastructure. The
implemented WWAN port operations are start, stop, tx, tx_blocking
and tx_poll.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
drivers/net/wwan/t9xx/mtk_port.c | 26 ++
drivers/net/wwan/t9xx/mtk_port.h | 13 +
drivers/net/wwan/t9xx/mtk_port_io.c | 335 +++++++++++++++++++++++++
drivers/net/wwan/t9xx/mtk_port_io.h | 5 +
drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c | 8 +
5 files changed, 387 insertions(+)
diff --git a/drivers/net/wwan/t9xx/mtk_port.c b/drivers/net/wwan/t9xx/mtk_port.c
index ee52b44ddf86..e67ac14c9286 100644
--- a/drivers/net/wwan/t9xx/mtk_port.c
+++ b/drivers/net/wwan/t9xx/mtk_port.c
@@ -819,6 +819,29 @@ int mtk_port_ch_disable(struct mtk_port *port)
return ret;
}
+static int mtk_port_enable_by_type(struct mtk_port_mngr *port_mngr, int tbl_type)
+{
+ struct mtk_port **ports;
+ int ret, idx;
+
+ if (tbl_type < 0 || tbl_type >= PORT_TBL_MAX)
+ return -EINVAL;
+
+ ports = kcalloc(port_mngr->port_cnt, sizeof(struct mtk_port *), GFP_KERNEL);
+ if (!ports)
+ return -ENOMEM;
+
+ ret = radix_tree_gang_lookup(&port_mngr->port_tbl[tbl_type],
+ (void **)ports, 0, port_mngr->port_cnt);
+ for (idx = 0; idx < ret; idx++) {
+ if (ports[idx]->enable)
+ ports_ops[ports[idx]->info.type]->enable(ports[idx]);
+ }
+
+ kfree(ports);
+ return 0;
+}
+
static void mtk_port_disable(struct mtk_port_mngr *port_mngr)
{
struct radix_tree_iter iter;
@@ -850,6 +873,9 @@ void mtk_port_mngr_fsm_state_handler(struct mtk_fsm_param *fsm_param, void *arg)
case FSM_STATE_OFF:
mtk_port_disable(port_mngr);
break;
+ case FSM_STATE_READY:
+ mtk_port_enable_by_type(port_mngr, PORT_TBL_MD);
+ break;
default:
break;
}
diff --git a/drivers/net/wwan/t9xx/mtk_port.h b/drivers/net/wwan/t9xx/mtk_port.h
index ba69eb37c7f9..f009715bf22c 100644
--- a/drivers/net/wwan/t9xx/mtk_port.h
+++ b/drivers/net/wwan/t9xx/mtk_port.h
@@ -56,6 +56,10 @@ enum mtk_ccci_ch {
/* to MD */
CCCI_CONTROL_RX = 0x2000,
CCCI_CONTROL_TX = 0x2001,
+ CCCI_UART2_RX = 0x200A,
+ CCCI_UART2_TX = 0x200C,
+ CCCI_MBIM_RX = 0x20D0,
+ CCCI_MBIM_TX = 0x20D1,
};
enum mtk_port_flag {
@@ -73,6 +77,7 @@ enum mtk_port_tbl {
enum mtk_port_type {
PORT_TYPE_INTERNAL,
+ PORT_TYPE_WWAN,
PORT_TYPE_MAX
};
@@ -81,6 +86,13 @@ struct mtk_internal_port {
int (*recv_cb)(void *arg, struct sk_buff *skb);
};
+struct mtk_wwan_port {
+ /* w_lock protects wwan_port when recv data and disable port at the same time */
+ struct mutex w_lock;
+ int w_type;
+ void *w_port;
+};
+
struct mtk_port_cfg {
enum mtk_ccci_ch tx_ch;
enum mtk_ccci_ch rx_ch;
@@ -110,6 +122,7 @@ struct mtk_port {
char dev_str[MTK_DEV_STR_LEN];
struct mtk_port_mngr *port_mngr;
struct mtk_internal_port i_priv;
+ struct mtk_wwan_port w_priv;
};
struct mtk_port_mngr {
diff --git a/drivers/net/wwan/t9xx/mtk_port_io.c b/drivers/net/wwan/t9xx/mtk_port_io.c
index a4e264cd5f63..c249b973bd09 100644
--- a/drivers/net/wwan/t9xx/mtk_port_io.c
+++ b/drivers/net/wwan/t9xx/mtk_port_io.c
@@ -3,6 +3,10 @@
* Copyright (c) 2022, MediaTek Inc.
*/
#include <linux/netdevice.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+#include <linux/wwan.h>
#include "mtk_port_io.h"
@@ -41,6 +45,145 @@ static void mtk_port_struct_init(struct mtk_port *port)
init_waitqueue_head(&port->rx_wq);
}
+static int mtk_port_copy_data_from(void *to, union user_buf from, unsigned int len,
+ unsigned int offset, bool from_user_space)
+{
+ if (from_user_space) {
+ if (copy_from_user(to, from.ubuf + offset, len))
+ return -EINVAL;
+ } else {
+ memcpy(to, from.kbuf + offset, len);
+ }
+
+ return 0;
+}
+
+static int mtk_port_common_write_frag_skb(struct mtk_port *port, struct sk_buff *skb,
+ union user_buf buf, u32 packet_size,
+ u32 cur_pos, bool from_user_space)
+{
+ struct sk_buff *frag_skb, *tmp = NULL;
+ u32 frag_size;
+ int ret;
+
+ frag_size = min(packet_size, port->tx_frag_size);
+ ret = mtk_port_copy_data_from(skb_put(skb, frag_size),
+ buf, frag_size,
+ cur_pos, from_user_space);
+ if (ret) {
+ dev_err(port->port_mngr->ctrl_blk->mdev->dev,
+ "Failed to copy skb for port(%s)\n", port->info.name);
+ goto err_reset_skb;
+ }
+ cur_pos += frag_size;
+ packet_size -= frag_size;
+ if (!packet_size)
+ return cur_pos;
+
+ while (packet_size > 0) {
+ frag_skb = __dev_alloc_skb(port->tx_mtu, GFP_KERNEL);
+ if (!frag_skb) {
+ ret = -ENOMEM;
+ goto err_free_frag_list;
+ }
+
+ frag_size = min(packet_size, port->tx_frag_size);
+ ret = mtk_port_copy_data_from(skb_put(frag_skb, frag_size),
+ buf, frag_size,
+ cur_pos, from_user_space);
+ if (ret) {
+ dev_err(port->port_mngr->ctrl_blk->mdev->dev,
+ "Failed to copy frag_skb for port(%s)\n", port->info.name);
+ dev_kfree_skb_any(frag_skb);
+ goto err_free_frag_list;
+ }
+ skb->data_len += frag_size;
+ skb->len += frag_size;
+ cur_pos += frag_size;
+ packet_size -= frag_size;
+ if (!tmp)
+ skb_shinfo(skb)->frag_list = frag_skb;
+ else
+ tmp->next = frag_skb;
+ tmp = frag_skb;
+ }
+ return cur_pos;
+
+err_free_frag_list:
+ frag_skb = skb_shinfo(skb)->frag_list;
+ while (frag_skb) {
+ tmp = frag_skb->next;
+ frag_skb->next = NULL;
+ dev_kfree_skb_any(frag_skb);
+ frag_skb = tmp;
+ }
+ skb_shinfo(skb)->frag_list = NULL;
+err_reset_skb:
+ skb->data_len = 0;
+ return ret;
+}
+
+static int mtk_port_common_write(struct mtk_port *port, union user_buf buf, unsigned int len,
+ bool from_user_space)
+{
+ u32 packet_size, left_cnt = len, cur_pos;
+ struct sk_buff *skb;
+ int ret;
+
+ if (len == 0)
+ return -EINVAL;
+
+start_write:
+ ret = mtk_port_status_check(port);
+ if (ret)
+ goto end_write;
+
+ skb = __dev_alloc_skb(port->tx_mtu, GFP_KERNEL);
+ if (!skb) {
+ ret = -ENOMEM;
+ goto end_write;
+ }
+
+ skb_reserve(skb, sizeof(struct mtk_ccci_header));
+
+ packet_size = min_t(u32, left_cnt, port->tx_mtu - sizeof(struct mtk_ccci_header));
+ cur_pos = len - left_cnt;
+ /* Support scatter gather transmission */
+ if (port->tx_mtu > port->tx_frag_size) {
+ ret = mtk_port_common_write_frag_skb(port, skb, buf, packet_size,
+ cur_pos, from_user_space);
+ if (ret < 0)
+ goto err_free_skb;
+ } else {
+ ret = mtk_port_copy_data_from(skb_put(skb, packet_size),
+ buf, packet_size,
+ cur_pos, from_user_space);
+ if (ret) {
+ dev_err(port->port_mngr->ctrl_blk->mdev->dev,
+ "Failed to copy data for port(%s)\n", port->info.name);
+ goto err_free_skb;
+ }
+ }
+
+ ret = mtk_port_send_data(port, skb);
+ if (ret < 0) {
+ if (ret == -EINTR)
+ left_cnt -= packet_size;
+ goto end_write;
+ }
+
+ left_cnt -= ret;
+ if (left_cnt)
+ goto start_write;
+ else
+ goto end_write;
+
+err_free_skb:
+ dev_kfree_skb_any(skb);
+end_write:
+ return (len > left_cnt) ? (len - left_cnt) : ret;
+}
+
static int mtk_port_internal_init(struct mtk_port *port)
{
mtk_port_struct_init(port);
@@ -233,6 +376,198 @@ static const struct port_ops port_internal_ops = {
.recv = mtk_port_internal_recv,
};
+static int mtk_port_wwan_open(struct wwan_port *w_port)
+{
+ struct mtk_port *port;
+ int ret;
+
+ port = wwan_port_get_drvdata(w_port);
+ ret = mtk_port_get_locked(port);
+ if (ret)
+ return ret;
+
+ ret = mtk_port_common_open(port);
+ if (ret)
+ mtk_port_put_locked(port);
+
+ return ret;
+}
+
+static void mtk_port_wwan_close(struct wwan_port *w_port)
+{
+ struct mtk_port *port = wwan_port_get_drvdata(w_port);
+
+ mtk_port_common_close(port);
+ mtk_port_put_locked(port);
+}
+
+static int mtk_port_wwan_write(struct wwan_port *w_port, struct sk_buff *skb)
+{
+ struct mtk_port *port = wwan_port_get_drvdata(w_port);
+ union user_buf user_buf;
+ int ret;
+
+ if (unlikely(!skb->len)) {
+ consume_skb(skb);
+ return 0;
+ }
+
+ port->info.flags &= ~PORT_F_BLOCKING;
+ user_buf.kbuf = (void *)skb->data;
+ ret = mtk_port_common_write(port, user_buf, skb->len, false);
+ if (ret < 0)
+ return ret;
+
+ consume_skb(skb);
+ return 0;
+}
+
+static int mtk_port_wwan_write_blocking(struct wwan_port *w_port, struct sk_buff *skb)
+{
+ struct mtk_port *port = wwan_port_get_drvdata(w_port);
+ union user_buf user_buf;
+ int ret;
+
+ if (unlikely(!skb->len)) {
+ consume_skb(skb);
+ return 0;
+ }
+
+ port->info.flags |= PORT_F_BLOCKING;
+ user_buf.kbuf = (void *)skb->data;
+ ret = mtk_port_common_write(port, user_buf, skb->len, false);
+ if (ret < 0)
+ return ret;
+
+ consume_skb(skb);
+ return 0;
+}
+
+static __poll_t mtk_port_wwan_poll(struct wwan_port *w_port, struct file *file,
+ struct poll_table_struct *poll)
+{
+ struct mtk_port *port = wwan_port_get_drvdata(w_port);
+ union ctrl_hif_cmd_data hif_cmd;
+ struct mtk_ctrl_blk *ctrl_blk;
+ __poll_t mask = 0;
+
+ poll_wait(file, &port->trb_wq, poll);
+ if (mtk_port_status_check(port))
+ return EPOLLERR | EPOLLHUP;
+
+ ctrl_blk = port->port_mngr->ctrl_blk;
+ hif_cmd.rx_ch = port->info.rx_ch;
+ if (!ctrl_blk->ops->send_cmd(ctrl_blk->mdev, HIF_CTRL_CMD_CHECK_TX_FULL, &hif_cmd))
+ mask |= EPOLLOUT | EPOLLWRNORM;
+
+ return mask;
+}
+
+static const struct wwan_port_ops wwan_ops = {
+ .start = mtk_port_wwan_open,
+ .stop = mtk_port_wwan_close,
+ .tx = mtk_port_wwan_write,
+ .tx_blocking = mtk_port_wwan_write_blocking,
+ .tx_poll = mtk_port_wwan_poll,
+};
+
+static int mtk_port_wwan_init(struct mtk_port *port)
+{
+ mtk_port_struct_init(port);
+ port->enable = false;
+
+ mutex_init(&port->w_priv.w_lock);
+
+ switch (port->info.rx_ch) {
+ case CCCI_MBIM_RX:
+ port->w_priv.w_type = WWAN_PORT_MBIM;
+ break;
+ case CCCI_UART2_RX:
+ port->w_priv.w_type = WWAN_PORT_AT;
+ break;
+ default:
+ port->w_priv.w_type = WWAN_PORT_UNKNOWN;
+ break;
+ }
+
+ return 0;
+}
+
+static void mtk_port_wwan_exit(struct mtk_port *port)
+{
+ if (test_bit(PORT_S_ENABLE, &port->status))
+ ports_ops[port->info.type]->disable(port);
+}
+
+static void mtk_port_wwan_enable(struct mtk_port *port)
+{
+ struct mtk_port_mngr *port_mngr;
+ int ret;
+
+ port_mngr = port->port_mngr;
+
+ if (test_bit(PORT_S_ENABLE, &port->status))
+ return;
+
+ ret = mtk_port_ch_enable(port);
+ if (ret && ret != -EBUSY)
+ return;
+
+ port->w_priv.w_port = wwan_create_port(port_mngr->ctrl_blk->mdev->dev,
+ port->w_priv.w_type,
+ &wwan_ops, NULL, port);
+ if (IS_ERR(port->w_priv.w_port)) {
+ dev_warn(port_mngr->ctrl_blk->mdev->dev,
+ "Failed to create wwan port for (%s)\n", port->info.name);
+ port->w_priv.w_port = NULL;
+ mtk_port_ch_disable(port);
+ return;
+ }
+
+ set_bit(PORT_S_WR, &port->status);
+ set_bit(PORT_S_ENABLE, &port->status);
+}
+
+static void mtk_port_wwan_disable(struct mtk_port *port)
+{
+ struct wwan_port *w_port;
+
+ if (!test_and_clear_bit(PORT_S_ENABLE, &port->status))
+ return;
+
+ clear_bit(PORT_S_WR, &port->status);
+ w_port = port->w_priv.w_port;
+ mutex_lock(&port->w_priv.w_lock);
+ port->w_priv.w_port = NULL;
+ mutex_unlock(&port->w_priv.w_lock);
+
+ mtk_port_ch_disable(port);
+ wwan_remove_port(w_port);
+}
+
+static int mtk_port_wwan_recv(struct mtk_port *port, struct sk_buff *skb)
+{
+ mutex_lock(&port->w_priv.w_lock);
+ if (!port->w_priv.w_port) {
+ mutex_unlock(&port->w_priv.w_lock);
+ return -ENXIO;
+ }
+
+ wwan_port_rx(port->w_priv.w_port, skb);
+ mutex_unlock(&port->w_priv.w_lock);
+ return 0;
+}
+
+static const struct port_ops port_wwan_ops = {
+ .init = mtk_port_wwan_init,
+ .exit = mtk_port_wwan_exit,
+ .reset = mtk_port_reset,
+ .enable = mtk_port_wwan_enable,
+ .disable = mtk_port_wwan_disable,
+ .recv = mtk_port_wwan_recv,
+};
+
const struct port_ops *ports_ops[PORT_TYPE_MAX] = {
&port_internal_ops,
+ &port_wwan_ops,
};
diff --git a/drivers/net/wwan/t9xx/mtk_port_io.h b/drivers/net/wwan/t9xx/mtk_port_io.h
index 0c10e893b7e0..ea92cd22dba0 100644
--- a/drivers/net/wwan/t9xx/mtk_port_io.h
+++ b/drivers/net/wwan/t9xx/mtk_port_io.h
@@ -23,6 +23,11 @@ struct port_ops {
int (*recv)(struct mtk_port *port, struct sk_buff *skb);
};
+union user_buf {
+ void __user *ubuf;
+ void *kbuf;
+};
+
void *mtk_port_internal_open(struct mtk_md_dev *mdev, char *name, int flag);
int mtk_port_internal_close(void *i_port);
int mtk_port_internal_write(void *i_port, struct sk_buff *skb);
diff --git a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
index 8611561dd67c..aab09cab360c 100644
--- a/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
+++ b/drivers/net/wwan/t9xx/pcie/mtk_ctrl_cfg_m9xx.c
@@ -16,6 +16,10 @@ static const int mtk_srv_cfg_m9xx[NR_CLDMA][HW_QUE_NUM] = {
/* the number of RX GPDs should be at last two */
static const struct queue_info mtk_queue_info_m9xx[] = {
+ {CCCI_UART2_TX, CCCI_UART2_RX, CLDMA1, TXQ(5), RXQ(5),
+ Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
+ {CCCI_MBIM_TX, CCCI_MBIM_RX, CLDMA1, TXQ(2), RXQ(2),
+ Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
{CCCI_CONTROL_TX, CCCI_CONTROL_RX, CLDMA1, TXQ(0), RXQ(0),
Q_MTU_3_5K, Q_MTU_3_5K, TX_GPD_NUM, RX_GPD_NUM, Q_FRAG_3_5K, Q_FRAG_3_5K, 0},
{CCCI_SAP_CONTROL_TX, CCCI_SAP_CONTROL_RX, CLDMA0, TXQ(0), RXQ(0),
@@ -23,6 +27,10 @@ static const struct queue_info mtk_queue_info_m9xx[] = {
};
static const struct mtk_port_cfg port_cfg_m9xx[] = {
+ {CCCI_UART2_TX, CCCI_UART2_RX, PORT_TYPE_WWAN, "AT",
+ PORT_F_ALLOW_DROP},
+ {CCCI_MBIM_TX, CCCI_MBIM_RX, PORT_TYPE_WWAN, "MBIM",
+ PORT_F_ALLOW_DROP},
{CCCI_CONTROL_TX, CCCI_CONTROL_RX, PORT_TYPE_INTERNAL, "MDCTRL",
PORT_F_ALLOW_DROP},
{CCCI_SAP_CONTROL_TX, CCCI_SAP_CONTROL_RX, PORT_TYPE_INTERNAL, "SAPCTRL",
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
` (5 preceding siblings ...)
2026-08-11 7:14 ` [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
@ 2026-08-11 7:14 ` Jack Wu via B4 Relay
2026-08-17 22:38 ` Jakub Kicinski
2026-08-17 22:39 ` [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jakub Kicinski
7 siblings, 1 reply; 15+ messages in thread
From: Jack Wu via B4 Relay @ 2026-08-11 7:14 UTC (permalink / raw)
To: Loic Poulain, Sergey Ryazanov, Johannes Berg, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Jack Wu, Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng,
Matthias Brugger, AngeloGioacchino Del Regno, Simon Horman,
Jonathan Corbet, Shuah Khan, Robert Yu, Jeff Chang
Cc: linux-kernel, netdev, linux-arm-kernel, linux-mediatek, linux-doc
From: Jack Wu <jackbb_wu@compal.com>
Add MAINTAINERS entry for the MediaTek T9XX 5G WWAN modem device
driver.
Signed-off-by: Jack Wu <jackbb_wu@compal.com>
---
MAINTAINERS | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 461a3eed6129..8f13ef7440ce 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16494,6 +16494,17 @@ L: netdev@vger.kernel.org
S: Supported
F: drivers/net/wwan/t7xx/
+MEDIATEK T9XX 5G WWAN MODEM DRIVER
+M: Jack Wu <jackbb_wu@compal.com>
+M: Robert Yu <robert_yu@compal.com>
+M: Jeff Chang <Jeff_Chang@compal.com>
+R: Wen-Zhi Huang <wen-zhi.huang@mediatek.com>
+R: Shi-Wei Yeh <shi-wei.yeh@mediatek.com>
+R: Minano Tseng <Minano.tseng@mediatek.com>
+L: netdev@vger.kernel.org
+S: Supported
+F: drivers/net/wwan/t9xx/
+
MEDIATEK USB3 DRD IP DRIVER
M: Chunfeng Yun <chunfeng.yun@mediatek.com>
L: linux-usb@vger.kernel.org
--
2.34.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
@ 2026-08-17 22:35 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:35 UTC (permalink / raw)
To: jackbb_wu
Cc: Jakub Kicinski, loic.poulain, ryazanov.s.a, johannes,
andrew+netdev, davem, edumazet, pabeni, wen-zhi.huang,
shi-wei.yeh, Minano.tseng, matthias.bgg,
angelogioacchino.delregno, horms, corbet, skhan, robert_yu,
Jeff_Chang, linux-kernel, netdev, linux-arm-kernel,
linux-mediatek, linux-doc
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add PCIe core
This adds a new MediaTek T9xx WWAN driver skeleton: the PCIe layer with
probe/remove and MSI-X setup, the MHCCIF event channel plumbing, device
reset paths (MHCCIF, ACPI _RST, ACPI PXP._OFF/_ON), and the Kconfig and
Makefile bits. A few questions below about the interrupt dispatch, event
channel translation and teardown ordering.
The commit message says:
Set up all the fundamental configurations for the device: PCIe layer,
Modem Host Cross Core Interface (MHCCIF), Reset Generation
Unit (RGU), modem common control operations and build
infrastructure.
and
* RGU provides interrupt channels to generate notifications
from the device so that the T900 driver could get the
device reset.
[Severity: Medium]
Is the RGU part accurate for this patch? Searching for rgu (case
insensitive) under drivers/net/wwan/t9xx/ returns nothing, either at this
commit or at the end of the series. mtk_pci_reg.h defines only PCIe MAC and
MHCCIF registers, enum mtk_irq_src has no RGU/reset-notification source, and
mtk_pci_probe() initialises only ATR and MHCCIF.
All resets present here are host initiated (MHCCIF
DEV_EVT_H2D_DEVICE_RESET, ACPI _RST via mtk_pci_fldr(), ACPI PXP._OFF/_ON
via mtk_pci_pldr()); there is no device to host reset notification channel.
Could the RGU wording be dropped or moved to the patch that actually adds
it?
> diff --git a/drivers/net/wwan/Kconfig b/drivers/net/wwan/Kconfig
> index 88df55d78d902..18bd40b3b8d21 100644
> --- a/drivers/net/wwan/Kconfig
> +++ b/drivers/net/wwan/Kconfig
> @@ -121,6 +121,18 @@ config MTK_T7XX
>
> If unsure, say N.
>
> +config MTK_T9XX
> + tristate "MediaTek PCIe 5G WWAN modem T9xx device"
> + depends on PCI && ACPI
> + select NET_DEVLINK
[Severity: Low]
What uses devlink here? Grepping for devlink under drivers/net/wwan/t9xx/
finds no match at this commit, and none at the tip of the series either. The
objects built for this config are only pcie/mtk_pci.o and
pcie/mtk_pci_drv_m9xx.o, so this select force-enables NET_DEVLINK for every
user of MTK_T9XX without a consumer. Can it be dropped until a devlink user
appears?
> + help
> + Enables MediaTek PCIe based 5G WWAN modem (T9xx series) device.
> +
> + To compile this driver as a module, choose M here: the module will be
> + called mtk_t9xx.
> +
> + If unsure, say N.
> +
> endif # WWAN
>
> endmenu
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> new file mode 100644
> index 0000000000000..1f6cca70b74c5
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> @@ -0,0 +1,1029 @@
[ ... ]
> + /* Calculate table offset */
> + offset = ATR_PORT_OFFSET * cfg->port + ATR_TABLE_OFFSET * cfg->table;
> + addr = REG_ATR_PCIE_WIN0_T0_SRC_ADDR_MSB + offset;
> + val = (u32)(cfg->src_addr >> 32);
> + mtk_pci_mac_write32(priv, addr, val);
> +
> + addr = REG_ATR_PCIE_WIN0_T0_SRC_ADDR_LSB + offset;
> + val = (u32)(cfg->src_addr & ATR_ADDR_ALIGN_MASK) | (atr_size << 1) | ATR_EN;
> + mtk_pci_mac_write32(priv, addr, val);
> +
> + addr = REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_MSB + offset;
> + val = (u32)(cfg->trsl_addr >> 32);
> + mtk_pci_mac_write32(priv, addr, val);
> +
> + addr = REG_ATR_PCIE_WIN0_T0_TRSL_ADDR_LSB + offset;
> + val = (u32)(cfg->trsl_addr & ATR_ADDR_ALIGN_MASK);
> + mtk_pci_mac_write32(priv, addr, val);
> +
> + /* TRSL_PARAM */
> + addr = REG_ATR_PCIE_WIN0_T0_TRSL_PARAM + offset;
> + val = (cfg->trsl_param << ATR_PARAM_OFFSET) | cfg->trsl_id;
> + mtk_pci_mac_write32(priv, addr, val);
[Severity: Medium]
Should ATR_EN be written last in mtk_pci_setup_atr()? The SRC_ADDR_LSB write
sets the enable bit while TRSL_ADDR_MSB/LSB and TRSL_PARAM still hold their
previous values, so the table is live with a stale translation target for
three more MMIO writes.
mtk_pci_atr_disable() clears only BIT(0), so the stale TRSL_ADDR/TRSL_PARAM
values are exactly what a prematurely enabled table would use. The four
AXI2PCI tables configured by the loop in mtk_pci_atr_init_m9xx() govern
device initiated DMA.
The t7xx driver programs the same IP in the opposite order, with an explicit
read back:
drivers/net/wwan/t7xx/t7xx_pcie_mac.c:t7xx_pcie_mac_atr_cfg() {
...
iowrite32(cfg->trsl_id, reg);
...
value = (cfg->src_addr & ...) | (atr_size << 1) | BIT(0);
iowrite64_lo_hi(value, reg);
/* Ensure ATR is set */
ioread64_lo_hi(reg);
}
Would writing TRSL_ADDR and TRSL_PARAM first, then SRC_ADDR_LSB with
ATR_EN, followed by a read back, be safer here too?
> +
> + return 0;
> +}
[ ... ]
> +static void mtk_pci_set_msix_merged(struct mtk_pci_priv *priv, int irq_cnt)
> +{
> + mtk_pci_mac_write32(priv, REG_PCIE_CFG_MSIX, ffs(irq_cnt) * 2 - 1);
> +}
[Severity: Medium]
Does anything restore REG_PCIE_CFG_MSIX? This is the only write to that
register, and it happens only when irq_cnt != MTK_IRQ_CNT_MAX in
mtk_pci_request_irq_msix().
The probe failure paths after IRQ setup (the -ENOLINK case and the
pci_store_saved_state() failure) free the vectors but leave the merge mode
programmed, and mtk_pci_remove() does not touch it either. A later probe
that gets all 32 vectors deliberately skips programming it, assuming
power-on unmerged mode.
mtk_pci_pldr() returns -ENODEV when the bridge has no PXP._OFF/_ON, and the
MHCCIF fallback may not reset the PCIe MAC, so can a rebind end up with 32
independent per-vector descriptors while the device is still in merged
routing mode?
[ ... ]
> +int mtk_pci_register_irq(struct mtk_md_dev *mdev, int irq_id,
> + int (*irq_cb)(int irq_id, void *data), void *data)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> +
> + if ((irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX) || !irq_cb)
> + return -EINVAL;
> +
> + if (priv->irq_cb_list[irq_id]) {
> + dev_err(mdev->dev,
> + "Unable to register irq, irq_id=%d, it's already been register by %ps.\n",
> + irq_id, priv->irq_cb_list[irq_id]);
> + return -EFAULT;
> + }
> + priv->irq_cb_list[irq_id] = irq_cb;
> + priv->irq_cb_data[irq_id] = data;
> +
> + return 0;
> +}
[Severity: Medium]
Can a hardirq observe a half published callback pair here? The callback
pointer is stored before its data pointer, with no lock, no WRITE_ONCE and
no barrier, while mtk_pci_irq_handler() reads and dereferences both from
hardirq context:
if (likely(priv->irq_cb_list[irq_id]))
priv->irq_cb_list[irq_id](irq_id, priv->irq_cb_data[irq_id]);
so a handler can call the new callback with a NULL or stale data argument.
At the tip of the series mtk_cldma_dev_init() calls mtk_pci_register_irq()
at runtime while other MSI-X vectors are already live, since
mtk_pci_dev_init() runs after mtk_pci_request_irq() in probe.
The "already registered" test is also a plain check-then-set, so two
concurrent registrations for the same irq_id both pass and the second
silently overwrites the first.
Would publishing data before the callback pointer, and taking a lock over
both arrays, be preferable?
[ ... ]
> +int mtk_pci_unregister_irq(struct mtk_md_dev *mdev, int irq_id)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> +
> + if (irq_id < 0 || irq_id >= MTK_IRQ_CNT_MAX)
> + return -EINVAL;
> +
> + if (!priv->irq_cb_list[irq_id]) {
> + dev_err(mdev->dev, "irq_id=%d has not been registered\n", irq_id);
> + return -EFAULT;
> + }
> + priv->irq_cb_list[irq_id] = NULL;
> + priv->irq_cb_data[irq_id] = NULL;
> +
> + return 0;
> +}
[ ... ]
> +static u32 mtk_pci_ext_h2d_evt_hw_bits(u32 chs)
> +{
> + u32 hw_bits = 0;
> +
> + SET_HW_BITS(hw_bits, chs, MHCCIF_RC2EP_EVT_DEVICE_RESET,
> + DEV_EVT_H2D_DEVICE_RESET);
> + return hw_bits;
> +}
> +
> +/**
> + * mtk_pci_send_ext_evt() - Send an MHCCIF event to the modem
> + * @mdev: MTK MD device
> + * @ch: Event channel to trigger (must be a single bit)
> + *
> + * Return: 0 on success, negative error code on failure.
> + */
> +int mtk_pci_send_ext_evt(struct mtk_md_dev *mdev, u32 ch)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> + u32 rc_base, hw_bits;
> +
> + rc_base = priv->cfg->mhccif_rc_base_addr;
> +
> + /* Only allow one ch to be triggered at a time */
> + if (!is_power_of_2(ch)) {
> + dev_err(mdev->dev, "Unsupported ext evt ch=0x%08x\n", ch);
> + return -EINVAL;
> + }
> +
> + hw_bits = mtk_pci_ext_h2d_evt_hw_bits(ch);
> + mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_BSY, hw_bits);
> + mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_TCHNUM, ffs(hw_bits) - 1);
> + return 0;
> +}
[Severity: Medium]
What happens when ch is a declared channel that has no hardware mapping?
mtk_pci_ext_h2d_evt_hw_bits() translates only DEV_EVT_H2D_DEVICE_RESET, but
enum mtk_pci_evt_h2d in mtk_pci.h declares CLDMA, DPMAIF and PCIe PM
suspend/resume channels as well.
For such a channel is_power_of_2(ch) passes, hw_bits ends up 0, and then:
mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_BSY, 0);
mtk_pci_write32(mdev, rc_base + MHCCIF_RC2EP_SW_TCHNUM, ffs(0) - 1);
so 0xFFFFFFFF is written to the channel number register and 0 (success) is
returned to the caller.
The D2H side has the same gap: mtk_pci_ext_d2h_evt_hw_bits() and
mtk_pci_ext_d2h_evt_chs() translate only BOOT_FLOW_SYNC,
ASYNC_HS_NOTIFY_SAP and ASYNC_HS_NOTIFY_MD, so
mtk_pci_mask_ext_evt()/mtk_pci_unmask_ext_evt()/mtk_pci_clear_ext_evt()
write hw_bits == 0 and silently do nothing for every other declared
channel, although their kernel-doc says they mask, unmask or clear the
requested channels. mtk_pci_register_ext_evt() likewise accepts masks that
can never be decoded.
At the tip of the series the only callers use mapped channels
(mtk_pci_dev_reset() uses DEV_EVT_H2D_DEVICE_RESET, mtk_fsm.c uses the three
mapped D2H channels), so the bogus write is not reachable today. Should
mtk_pci_send_ext_evt() return an error when hw_bits is 0, and should the
enums be trimmed to the channels that are actually translated?
[ ... ]
> +static void mtk_mhccif_isr_work(struct work_struct *work)
> +{
> + struct mtk_pci_priv *priv =
> + container_of(work, struct mtk_pci_priv, mhccif_work);
> + struct mtk_md_dev *mdev = priv->irq_desc->mdev;
> + struct mtk_mhccif_cb *cb;
> + u32 stat, mask, chs;
> +
> + stat = mtk_pci_get_ext_evt_hw_status(mdev);
> + mask = mtk_pci_read32(mdev, priv->cfg->mhccif_rc_base_addr
> + + MHCCIF_EP2RC_SW_INT_EAP_MASK);
> + if (unlikely(stat == U32_MAX && !(mtk_pci_link_check(mdev)))) {
> + /* When link failed, we don't need to unmask/clear. */
> + dev_err(mdev->dev, "Failed to check link in MHCCIF handler.\n");
> + return;
> + }
> +
> + stat &= ~mask;
> + chs = mtk_pci_ext_d2h_evt_chs(stat);
> + spin_lock_bh(&priv->mhccif_lock);
> + list_for_each_entry(cb, &priv->mhccif_cb_list, entry) {
> + if (cb->chs & chs)
> + cb->evt_cb(cb->chs & chs, cb->data);
> + }
> + spin_unlock_bh(&priv->mhccif_lock);
[Severity: Low]
Is it intended that consumer callbacks run in atomic context under
mhccif_lock? A callback that sleeps is not allowed here, and a callback that
calls mtk_pci_register_ext_evt() or mtk_pci_unregister_ext_evt() would
deadlock on the same non-recursive lock. The iteration also uses
list_for_each_entry() rather than the _safe variant.
No consumer in the series hits this: the only registered callbacks at the
tip are mtk_fsm_early_bootup_handler() and mtk_fsm_hs1_handler(), both of
which only do MMIO and call mtk_fsm_evt_submit() with mode 0, which picks
GFP_ATOMIC when in_softirq() is true and only waits for EVT_MODE_BLOCKING.
Neither touches mhccif_cb_list. Could the callback contract be documented,
or the callbacks invoked outside the lock?
> +
> + mtk_pci_clear_irq(mdev, priv->mhccif_irq_id);
> + mtk_pci_unmask_irq(mdev, priv->mhccif_irq_id);
> +}
[ ... ]
> +static void mtk_mhccif_exit(struct mtk_md_dev *mdev)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> +
> + mtk_pci_unregister_irq(mdev, priv->mhccif_irq_id);
> + cancel_work_sync(&priv->mhccif_work);
> +}
[Severity: Low]
Should mtk_mhccif_exit() also drain priv->mhccif_cb_list? The nodes are
kzalloc'ed in mtk_pci_register_ext_evt(), priv itself is devm freed after
remove, and mtk_pci_unregister_ext_evt() frees a node only on an exact
cb->chs == chs match, so a partial unregister just emits a dev_warn() and
leaks the node.
Nothing leaks today: the only registrations at the tip of the series are
DEV_EVT_D2H_BOOT_FLOW_SYNC in mtk_fsm.c and hs_info->mhccif_ch in
mtk_fsm_hs_info_init(), and both are unregistered with the identical mask by
mtk_fsm_exit() and mtk_fsm_hs_info_exit(). Would freeing any remaining
nodes in mtk_mhccif_exit() make the teardown self contained?
> +
> +static irqreturn_t mtk_pci_irq_handler(struct mtk_md_dev *mdev, u32 irq_state)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> + int irq_id;
> +
> + /* Check whether each set bit has a callback, if has, call it */
> + do {
> + irq_id = fls(irq_state) - 1;
> + irq_state &= ~BIT(irq_id);
> + if (likely(priv->irq_cb_list[irq_id]))
> + priv->irq_cb_list[irq_id](irq_id, priv->irq_cb_data[irq_id]);
> + else
> + dev_err(mdev->dev, "Unhandled irq_id=%d, no callback for it.\n", irq_id);
> + } while (irq_state);
> +
> + return IRQ_HANDLED;
> +}
> +
> +static irqreturn_t mtk_pci_irq_msix(int irq, void *data)
> +{
> + struct mtk_pci_irq_desc *irq_desc = data;
> + struct mtk_md_dev *mdev = irq_desc->mdev;
> + struct mtk_pci_priv *priv;
> + u32 irq_state, irq_enable;
> +
> + priv = mdev->hw_priv;
> + irq_state = mtk_pci_mac_read32(priv, REG_MSIX_ISTATUS_HOST_GRP0_0);
> + irq_enable = mtk_pci_mac_read32(priv, REG_IMASK_HOST_MSIX_GRP0_0);
> + irq_state &= irq_enable;
[Severity: Medium]
Should these two reads be validated for the device-gone case? When the
endpoint is unreachable (surprise removal, FLDR/PLDR in progress, link
retraining) both reads return 0xFFFFFFFF, irq_state becomes U32_MAX, both
guards below pass, and mtk_pci_irq_handler() then dispatches the callbacks
of all 32 sources as if every one were pending. The subsequent mask write to
REG_IMASK_HOST_MSIX_CLR_GRP0_0 is dropped, so the "user needs to unmask by
itself" protocol no longer holds.
mtk_pci_remove() masks only the MHCCIF vector before calling
mtk_pci_pldr(), leaving the other 31 vectors armed across the ACPI power
cycle. mtk_mhccif_isr_work() already tests for this case with
'stat == U32_MAX && !mtk_pci_link_check(mdev)' - should the MSI-X handler do
the same?
> +
> + if (unlikely(!irq_state) ||
> + unlikely(!((irq_state & GENMASK(priv->irq_cnt - 1, 0)) &
> + irq_desc->msix_bits)))
> + return IRQ_NONE;
> +
> + /* Mask the bit and user needs to unmask by itself */
> + mtk_pci_mac_write32(priv, REG_IMASK_HOST_MSIX_CLR_GRP0_0,
> + irq_state & ~BIT(30));
> +
> + return mtk_pci_irq_handler(mdev, irq_state);
> +}
[Severity: High]
Can the same consumer callback run concurrently on two CPUs here? The gate
only checks that this vector's own bit is among the pending bits, but the
full bitmap is then passed to mtk_pci_irq_handler(), which invokes the
callback of every set bit.
With DPMAIF (bit 24) and CLDMA0 (bit 27) pending at the same time, two
MSI-X vectors fire on two CPUs, both latch the same irq_state (the status
read and the mask write are not atomic and nothing serialises the handlers),
and both dispatch all pending callbacks.
This also defeats the consumer teardown protocol:
drivers/net/wwan/t9xx/pcie/mtk_cldma.c:mtk_cldma_dev_exit() {
...
mtk_pci_mask_irq(mdev, drv_info->pci_ext_irq_id);
synchronize_irq(virq_id);
...
kfree(drv_info);
}
synchronize_irq() covers only its own vector, so a handler on another vector
can still be running mtk_cldma_isr(irq_id, drv_info) after the free. Would
passing 'irq_state & irq_desc->msix_bits' to mtk_pci_irq_handler(), or
registering one handler per vector as t7xx_request_irq() does, avoid this?
> +
> +static int mtk_pci_request_irq_msix(struct mtk_md_dev *mdev,
> + int irq_cnt_allocated)
> +{
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> + struct mtk_pci_irq_desc *irq_desc;
> + struct pci_dev *pdev;
> + int irq_cnt;
> + int ret, i;
> +
> + /* calculate the nearest 2's power number */
> + irq_cnt = BIT(fls(irq_cnt_allocated) - 1);
> + pdev = to_pci_dev(mdev->dev);
> + irq_desc = priv->irq_desc;
> + for (i = 0; i < irq_cnt; i++) {
> + irq_desc[i].mdev = mdev;
> + irq_desc[i].msix_bits = BIT(i);
> + snprintf(irq_desc[i].name, MTK_IRQ_NAME_LEN, "msix%d-%s", i, mdev->dev_str);
> + ret = pci_request_irq(pdev, i, mtk_pci_irq_msix, NULL,
> + &irq_desc[i], "%s", irq_desc[i].name);
> + if (ret) {
> + dev_err(mdev->dev, "Failed to request %s: ret=%d\n",
> + irq_desc[i].name, ret);
> + for (i--; i >= 0; i--)
> + pci_free_irq(pdev, i, &irq_desc[i]);
> + return ret;
> + }
> + }
> + priv->irq_cnt = irq_cnt;
> + priv->irq_type = PCI_IRQ_MSIX;
[Severity: Medium]
Is there a window where a handler runs before irq_cnt and irq_type are
published? pci_request_irq() makes vector 0 live immediately, but both
fields are only assigned after the loop, so an interrupt taken inside the
loop enters mtk_pci_irq_msix() with priv->irq_cnt == 0 and evaluates:
irq_state & GENMASK(priv->irq_cnt - 1, 0)
which is GENMASK(-1, 0), i.e. a shift by BITS_PER_LONG (the input check is
inert for a non-constant high bit), so the mask is undefined.
If the handler then proceeds, mtk_mhccif_irq_cb() queues mhccif_work and
mtk_mhccif_isr_work() calls mtk_pci_clear_irq() and mtk_pci_unmask_irq(),
both of which return -EINVAL with a dev_err because priv->irq_type is still
0 while PCI_IRQ_MSIX is 4. The MHCCIF status bit then stays masked and
unacknowledged for the life of the device.
The window is open because the MHCCIF callback is already registered by
mtk_mhccif_init() and device interrupts are not masked first
(mtk_dev_cfg_0900 never sets MTK_CFG_IRQ_DFLT_MASK, so the "mask all irqs"
write in probe is skipped). Could irq_cnt and irq_type be set before the
first pci_request_irq() call?
> +
> + if (irq_cnt != MTK_IRQ_CNT_MAX)
> + mtk_pci_set_msix_merged(priv, irq_cnt);
> +
> + return 0;
> +}
[Severity: High]
Does the dispatch path work at all when fewer than 32 vectors are obtained?
mtk_pci_request_irq() accepts anything from MTK_IRQ_CNT_MIN (1) upwards,
this function rounds down to a power of two and programs merged mode, but
the descriptors keep msix_bits = BIT(i) and mtk_pci_irq_handler() indexes
priv->irq_cb_list[] by the raw device bit number.
In mtk_pci_irq_msix() every status bit >= irq_cnt is discarded:
if (unlikely(!irq_state) ||
unlikely(!((irq_state & GENMASK(priv->irq_cnt - 1, 0)) &
irq_desc->msix_bits)))
return IRQ_NONE;
MHCCIF is irq_id 28 in mtk_dev_cfg_0900.irq_tbl and is the only interrupt
probe unmasks, so with, say, 16 vectors it is either rejected with IRQ_NONE
(status bit never cleared, event lost, and possibly a "nobody cared"
shutdown of the shared line) or, if the hardware folds the bit down,
dispatched at a low index with no registered callback ("Unhandled irq_id").
CLDMA (26, 27) and DPMAIF (24, 29, 31) behave the same way.
mtk_pci_get_virq_id() uses 'irq_id % priv->irq_cnt', which suggests merged
mode is meant to work, but the dispatch side does not implement that
mapping. Would requiring an exact vector count, as t7xx_setup_msix() does
with pci_alloc_irq_vectors(pdev, EXT_INT_NUM, EXT_INT_NUM, PCI_IRQ_MSIX), be
the simpler contract?
[ ... ]
> +static void mtk_pci_remove(struct pci_dev *pdev)
> +{
> + struct mtk_md_dev *mdev = pci_get_drvdata(pdev);
> + struct mtk_pci_priv *priv = mdev->hw_priv;
> + struct device *dev = &pdev->dev;
> +
> + mtk_pci_mask_irq(mdev, priv->mhccif_irq_id);
> +
> + if (mtk_pci_pldr(mdev)) {
> + dev_warn(dev, "Failed to execute PLDR, try external event\n");
> + mtk_pci_reset(mdev, RESET_MHCCIF);
> + }
> +
> + pci_clear_master(pdev);
> + mtk_pci_free_irq(mdev);
> + mtk_mhccif_exit(mdev);
> + pci_load_and_free_saved_state(pdev, &priv->saved_state);
> +}
[Severity: Medium]
Can mhccif_work still be queued or running when the device is reset here?
mtk_pci_pldr() evaluates PXP._OFF, sleeps 500 ms and evaluates PXP._ON (or
the fallback mtk_pci_reset(mdev, RESET_MHCCIF) runs), while
cancel_work_sync() only happens later inside mtk_mhccif_exit(), after
pci_clear_master() and mtk_pci_free_irq().
A worker running concurrently does MMIO on a device being power cycled and
finishes with:
mtk_pci_clear_irq(mdev, priv->mhccif_irq_id);
mtk_pci_unmask_irq(mdev, priv->mhccif_irq_id);
re-enabling the vector that remove() just masked for the rest of teardown.
The early return in mtk_mhccif_isr_work() only fires when the status read is
U32_MAX and pci_device_is_present() fails, so it does not cover a worker
that already read a valid status before the reset started.
Would mask, then free/quiesce the IRQ, then cancel_work_sync(), then reset
be the right order?
[ ... ]
--
pw-bot: cr
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer
2026-08-11 7:14 ` [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
@ 2026-08-17 22:36 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:36 UTC (permalink / raw)
To: jackbb_wu
Cc: Jakub Kicinski, loic.poulain, ryazanov.s.a, johannes,
andrew+netdev, davem, edumazet, pabeni, wen-zhi.huang,
shi-wei.yeh, Minano.tseng, matthias.bgg,
angelogioacchino.delregno, horms, corbet, skhan, robert_yu,
Jeff_Chang, linux-kernel, netdev, linux-arm-kernel,
linux-mediatek, linux-doc
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add control plane transaction layer
This patch adds the mtk_ctrl_blk and mtk_ctrl_trans data structures along
with mtk_ctrl_init()/mtk_ctrl_exit() for the T9XX WWAN driver. It also
splits the build into a separate pcie/ Makefile gated by a new hidden
MTK_T9XX_PCI symbol.
A couple of questions below about the module split and the mtk_ctrl_exit()
documentation.
> diff --git a/drivers/net/wwan/Kconfig b/drivers/net/wwan/Kconfig
> index 18bd40b3b8d21..a2ffed346acc9 100644
> --- a/drivers/net/wwan/Kconfig
> +++ b/drivers/net/wwan/Kconfig
> @@ -124,6 +124,7 @@ config MTK_T7XX
> config MTK_T9XX
> tristate "MediaTek PCIe 5G WWAN modem T9xx device"
> depends on PCI && ACPI
> + select MTK_T9XX_PCI
> select NET_DEVLINK
> help
> Enables MediaTek PCIe based 5G WWAN modem (T9xx series) device.
> @@ -133,6 +134,10 @@ config MTK_T9XX
>
> If unsure, say N.
>
> +config MTK_T9XX_PCI
> + tristate
> + depends on PCI
> +
> endif # WWAN
>
> endmenu
> diff --git a/drivers/net/wwan/t9xx/Makefile b/drivers/net/wwan/t9xx/Makefile
> index 6f2dd3f914543..ae9d6f2344ab4 100644
> --- a/drivers/net/wwan/t9xx/Makefile
> +++ b/drivers/net/wwan/t9xx/Makefile
> @@ -4,7 +4,8 @@ ccflags-y += -I$(src)/pcie
> ccflags-y += -I$(src)
>
> obj-$(CONFIG_MTK_T9XX) += mtk_t9xx.o
> +obj-$(CONFIG_MTK_T9XX_PCI) += pcie/
>
> mtk_t9xx-y := \
> - pcie/mtk_pci.o \
> - pcie/mtk_pci_drv_m9xx.o
> + mtk_dev.o \
> + mtk_ctrl_plane.o
[Severity: Low]
Should the commit message mention this module split? Before this patch
mtk_t9xx-y contained pcie/mtk_pci.o and pcie/mtk_pci_drv_m9xx.o, so
mtk_t9xx.ko was the module carrying module_pci_driver() and
MODULE_DEVICE_TABLE(pci, ...) from mtk_pci.c.
After this patch those two objects move into a second module built from
pcie/Makefile:
obj-$(CONFIG_MTK_T9XX_PCI) += mtk_t9xx_pcie.o
mtk_t9xx_pcie-y := \
mtk_pci_drv_m9xx.o \
mtk_pci.o
so the module that binds the hardware and carries the PCI modaliases is
now mtk_t9xx_pcie.ko, while mtk_t9xx.ko only holds mtk_dev.o and
mtk_ctrl_plane.o with no driver registration.
The MTK_T9XX help text still says:
To compile this driver as a module, choose M here: the module will be
called mtk_t9xx.
Should that be updated to name mtk_t9xx_pcie as well? The new hidden
MTK_T9XX_PCI symbol, which is what actually gates building the PCI driver
objects, is not described anywhere either. Both the split and the help
text are still in this shape at the end of the series (cbf822bf).
> diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> new file mode 100644
> index 0000000000000..cf6079218e1ce
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> @@ -0,0 +1,45 @@
[ ... ]
> +int mtk_ctrl_init(struct mtk_md_dev *mdev)
> +{
> + struct mtk_ctrl_blk *ctrl_blk;
> +
> + ctrl_blk = devm_kzalloc(mdev->dev, sizeof(*ctrl_blk), GFP_KERNEL);
> + if (!ctrl_blk)
> + return -ENOMEM;
> +
> + ctrl_blk->mdev = mdev;
> + mdev->ctrl_blk = ctrl_blk;
> +
> + return 0;
> +}
> +EXPORT_SYMBOL(mtk_ctrl_init);
> +
> +/**
> + * mtk_ctrl_exit() - Clean up the control plane block.
> + * @mdev: Pointer to the MTK modem device.
> + *
> + * Frees the control plane block associated with @mdev.
> + */
> +void mtk_ctrl_exit(struct mtk_md_dev *mdev)
> +{
> + mdev->ctrl_blk = NULL;
> +}
> +EXPORT_SYMBOL(mtk_ctrl_exit);
[Severity: Low]
Does the kernel-doc for mtk_ctrl_exit() match what the function does? The
comment says "Frees the control plane block associated with @mdev", but the
body only clears the pointer:
void mtk_ctrl_exit(struct mtk_md_dev *mdev)
{
mdev->ctrl_blk = NULL;
}
The block comes from devm_kzalloc(mdev->dev, ...) in mtk_ctrl_init(), so it
stays on the device's devres list until driver detach.
Tracing the callers at the end of the series (cbf822bf), mtk_ctrl_init() is
only reached via mtk_pci_probe()->mtk_pci_dev_init()->mtk_trans_ctrl_init(),
and mtk_ctrl_exit() only from the probe error path and mtk_pci_remove(), so
there is one init/exit per device lifetime and nothing is actually leaked
here.
The wording is still "Frees the control plane block" at cbf822bf, where the
body unregisters the FSM notifier, calls mtk_port_mngr_exit() and nulls the
pointer. Could the doc be reworded to say the pointer is cleared and the
allocation is released by devres, so a later patch adding an explicit
devm_kfree() does not end up double freeing it?
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface
2026-08-11 7:14 ` [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
@ 2026-08-17 22:36 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:36 UTC (permalink / raw)
To: jackbb_wu
Cc: Jakub Kicinski, loic.poulain, ryazanov.s.a, johannes,
andrew+netdev, davem, edumazet, pabeni, wen-zhi.huang,
shi-wei.yeh, Minano.tseng, matthias.bgg,
angelogioacchino.delregno, horms, corbet, skhan, robert_yu,
Jeff_Chang, linux-kernel, netdev, linux-arm-kernel,
linux-mediatek, linux-doc
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add control DMA interface
This adds the CLDMA hardware interface used by the control plane, together
with the TRB service threads and PCIe transport glue that drive the GPD
rings. The new code allocates TX/RX queues, maps SKB payloads for DMA and
completes requests back to the port layer.
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> new file mode 100644
> index 0000000000000..097b20d8fbd38
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> @@ -0,0 +1,1232 @@
[ ... ]
> +static void mtk_cldma_rx_done_work(struct work_struct *work)
> +{
> + struct rx_req *req = NULL, *pre_req = NULL;
> + struct rxq *rxq = container_of(work, struct rxq, rx_done_work);
> + struct cldma_drv_info *drv_info;
> + struct cldma_drv_ops *drv_ops;
> + struct mtk_md_dev *mdev;
> + struct sk_buff *rx_skb;
> + int i, ret, idx;
> +
> + drv_info = rxq->drv_info;
> + mdev = drv_info->mdev;
> + drv_ops = drv_info->drv_ops;
> +
> +again:
> + for (i = 0; i < rxq->nr_gpds; i++) {
> + req = rxq->req_pool + rxq->free_idx;
> + if (!req->skb) {
> + dev_err(mdev->dev,
> + "Failed to get valid req cldma%d rxq%d req%d\n",
> + drv_info->hw_id, rxq->rxqno, rxq->free_idx);
> + goto out;
> + }
> +
> + if (req->gpd->rx_gpd.gpd_flags & CLDMA_GPD_FLAG_HWO)
> + break;
> +
> + dma_rmb(); /* read descriptor fields after HWO check */
> +
> + mtk_cldma_rx_skb_adjust(mdev, rxq, req);
> + rx_skb = req->skb;
> + req->skb = NULL;
> +
> + ret = mtk_cldma_reload_rx_skb(mdev, rxq, req);
> + if (ret) {
> + /* Alloc failed — recycle old buffer, drop packet.
> + * BD mode cannot recycle directly (BDP flag mismatch),
> + * so accept the stall and let reset recovery handle it.
> + */
> + if (rxq->nr_bds) {
> + dev_kfree_skb_any(rx_skb);
> + goto out;
> + }
> +
> + skb_trim(rx_skb, 0);
> + req->skb = rx_skb;
> + req->data_dma_addr = dma_map_single(mdev->dev,
> + rx_skb->data,
> + req->mtu,
> + DMA_FROM_DEVICE);
> + if (dma_mapping_error(mdev->dev, req->data_dma_addr)) {
> + req->data_dma_addr = 0;
> + dev_kfree_skb_any(rx_skb);
> + req->skb = NULL;
> + /* Advance free_idx below to prevent ring stall */
> + } else {
[Severity: High]
Can the device DMA into freed memory here? When both the refill in
mtk_cldma_reload_rx_skb() and this fallback re-map fail, req->skb and
req->data_dma_addr are cleared, but the descriptor still carries the old
programmed address:
mtk_cldma_rx_skb_adjust() {
if (req->data_dma_addr) {
dma_unmap_single(mdev->dev, req->data_dma_addr,
req->mtu, DMA_FROM_DEVICE);
req->data_dma_addr = 0;
}
...
}
The else branch that reprograms rx_gpd.data_buff_ptr_h/l is skipped, so
gpd->rx_gpd still points at the buffer that was just unmapped and freed
with dev_kfree_skb_any(rx_skb).
Control then falls through to the arming code below, which arms the
previous slot unconditionally, so on the next processed request this
poisoned descriptor becomes pre_req and is handed back to the hardware.
Are the two failures independent? If the first failure was itself a
dma_map_single() failure from IOVA or swiotlb exhaustion, the immediate
retry of the same mapping would fail too, which makes this path
reachable under memory pressure.
> + req->gpd->rx_gpd.data_buff_ptr_h =
> + cpu_to_le32((u64)req->data_dma_addr >> 32);
> + req->gpd->rx_gpd.data_buff_ptr_l =
> + cpu_to_le32(req->data_dma_addr);
> + }
> + } else {
> + do {
> + ret = rxq->rx_done(rx_skb, rxq->arg,
> + atomic_read(&rxq->need_exit) ? true : false);
> + if (ret == -EAGAIN)
> + usleep_range(1000, 2000);
> + } while (ret == -EAGAIN);
> + }
> +
> + wmb(); /* ensure addr set done before HWO setup done */
> +
> + idx = rxq->free_idx == 0 ? rxq->nr_gpds - 1 : rxq->free_idx - 1;
> + pre_req = rxq->req_pool + idx;
> + pre_req->gpd->rx_gpd.gpd_flags |= CLDMA_GPD_FLAG_HWO;
> + rxq->free_idx = (rxq->free_idx + 1) % rxq->nr_gpds;
Also, is the comment "Advance free_idx below to prevent ring stall"
inverted? Advancing past a slot with no buffer leaves req->skb NULL, and
on the next wrap-around the check at the top of this loop:
if (!req->skb) {
dev_err(mdev->dev, ...);
goto out;
}
aborts the RX work permanently for that queue, which looks like exactly
the stall the comment says it is preventing.
> + }
[ ... ]
> +static struct rxq *mtk_cldma_rxq_alloc(struct cldma_drv_info *drv_info, struct sk_buff *skb)
> +{
> + struct trb_open_priv *trb_open_priv = (struct trb_open_priv *)skb->data;
[ ... ]
> + INIT_WORK(&rxq->rx_done_work, mtk_cldma_rx_done_work);
> +
> + drv_info->rxq[rxq->rxqno] = rxq;
> + drv_ops->cldma_stop_queue(drv_info, DIR_RX, rxq->rxqno);
> + drv_ops->cldma_setup_start_addr(drv_info, DIR_RX,
> + rxq->rxqno, rxq->req_pool[0].gpd_dma_addr);
> + drv_ops->cldma_start_queue(drv_info, DIR_RX, rxq->rxqno);
[Severity: Medium]
Should the result of cldma_stop_queue() be checked here and in the other
three call sites? mtk_cldma_stop_queue() polls with read_poll_timeout()
but drops the -ETIMEDOUT and returns only the last sampled status:
mtk_cldma_drv.c:mtk_cldma_stop_queue() {
mtk_pci_write32(drv_info->mdev, addr, val);
read_poll_timeout(mtk_cldma_queue_status, active,
active == LINK_ERROR_VAL || !active,
WAIT_QUEUE_STOP, WAIT_QUEUE_STOP * 10, false,
drv_info, dir, qno);
return active;
}
WAIT_QUEUE_STOP is 70, so the budget is roughly 700 us. None of
mtk_cldma_txq_alloc(), mtk_cldma_rxq_alloc(), mtk_cldma_txq_free() or
mtk_cldma_rxq_free() looks at the value.
If a busy or hung modem does not stop the queue within that window, the
free paths go on to dma_unmap_single() the payload buffers and
dma_pool_free() the GPD and BD descriptors while the engine may still be
fetching them. synchronize_irq() only quiesces the interrupt handler, not
the DMA engine. Would returning the timeout and bailing out (or forcing a
CLDMA reset) be safer here?
> + drv_ops->cldma_unmask_intr(drv_info, DIR_RX, rxq->rxqno, QUEUE_ERROR);
> + drv_ops->cldma_unmask_intr(drv_info, DIR_RX, rxq->rxqno, QUEUE_XFER_DONE);
> +
> + return rxq;
[ ... ]
> +static int mtk_cldma_start_xfer(struct cldma_drv_info *drv_info, u32 qno)
> +{
> + struct cldma_drv_ops *drv_ops;
> + struct txq *txq;
> + u32 val;
> +
> + txq = drv_info->txq[qno];
> + drv_ops = drv_info->drv_ops;
> +
> + val = drv_ops->cldma_get_tx_start_addr(drv_info, qno);
> + if (unlikely(val == LINK_ERROR_VAL))
> + return -EIO;
> +
> + if (unlikely(!val)) {
> + drv_ops->cldma_drv_init(drv_info);
> + txq = drv_info->txq[qno];
> + drv_ops->cldma_setup_start_addr(drv_info, DIR_TX, qno,
> + txq->req_pool[txq->free_idx].gpd_dma_addr);
> + drv_ops->cldma_start_queue(drv_info, DIR_TX, qno);
> + txq->tx_started = true;
> + } else if (unlikely(!txq->tx_started)) {
[Severity: Medium]
Are txq->free_idx and txq->tx_started safe to read here without any
annotation or lock? This function runs from the TRB service thread via
mtk_cldma_tx(), while mtk_cldma_tx_done_work() advances the same field
from workqueue context with a plain store:
mtk_cldma_tx_done_work() {
...
txq->free_idx = (txq->free_idx + 1) % txq->nr_gpds;
}
The reader above uses txq->req_pool[txq->free_idx].gpd_dma_addr to
re-program the UL start address, so a stale index could point the hardware
at a GPD whose HWO bit has already been cleared.
Interesting that tx_done_work() itself uses READ_ONCE()/WRITE_ONCE() for
free_idx and wr_idx, which suggests the fields are known to be shared -
was the reader here just missed?
[Severity: Low]
Is zero a safe sentinel for "queue not programmed" given only the low half
of the address is read?
mtk_cldma_drv.c:mtk_cldma_get_tx_start_addr() {
addr = drv_info->base_addr + drv_info->hw_regs->reg_cldma_ul_start_addrl_0 +
qno * HW_QUEUE_NUM;
val = mtk_pci_read32(drv_info->mdev, addr);
return val;
}
mtk_cldma_setup_start_addr() programs both ADDRL and ADDRH and
mtk_cldma_drv_init() puts CLDMA into 64-bit GPD mode, so a GPD DMA address
of, say, 0x1_0000_0000 would read back as zero here and trigger a full
cldma_drv_init() plus queue restart. Should the high word be read too, as
the analogous t7xx code does?
> + drv_ops->cldma_start_queue(drv_info, DIR_TX, qno);
> + txq->tx_started = true;
> + } else {
> + drv_ops->cldma_resume_queue(drv_info, DIR_TX, qno);
> + }
> +
> + return 0;
> +}
[ ... ]
> +static int mtk_cldma_open(struct cldma_dev *cd, struct sk_buff *skb)
> +{
> + struct trb_open_priv *trb_open_priv = (struct trb_open_priv *)skb->data;
> + struct trb *trb = (struct trb *)skb->cb;
> + struct cldma_drv_info *drv_info;
> + struct queue_info *que;
> + struct txq *txq;
> + struct rxq *rxq;
> + int ret = 0;
> +
> + que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
> + drv_info = cd->cldma_drv_info[que->hif_id];
> + if (!drv_info) {
> + ret = -EIO;
> + goto out;
> + }
[Severity: Medium]
Can a failure here permanently poison the channel? mtk_ch_status_check()
already incremented the user count before this function runs:
mtk_trans_ctrl.c:mtk_ch_status_check() {
case TRB_CMD_ENABLE:
...
trans->usr_cnt[que->hif_id][que->txqno]++;
if (trans->usr_cnt[que->hif_id][que->txqno] == 1)
break;
...
}
and mtk_ctrl_trb_handler() discards the dispatch result:
if (kick) {
mtk_cldma_trb_process(trans->dev, skb);
trans_list->tx_burst_cnt[qno] = 0;
kick = false;
}
So on -EIO here, on -EINVAL for a zero MTU, or on -ENOMEM from
mtk_cldma_txq_alloc()/mtk_cldma_rxq_alloc(), usr_cnt stays at 1 with no
txq or rxq. A retried ENABLE then takes the usr_cnt != 1 path and
mtk_cldma_check_ch_cfg() fails with -EINVAL because txq and rxq are NULL.
Since usr_cnt lives in the devm-allocated struct mtk_ctrl_trans and is not
re-zeroed by mtk_pcie_hif_init(), does the channel stay unusable until the
device is removed?
> +
> + if (que->tx_mtu == 0 || que->rx_mtu == 0) {
[ ... ]
> +static int mtk_cldma_close(struct cldma_dev *cd, struct sk_buff *skb)
> +{
> + struct trb *trb = (struct trb *)skb->cb;
> + struct cldma_drv_info *drv_info;
> + struct queue_info *que;
> +
> + que = radix_tree_lookup(&cd->trans->queue_tbl, trb->channel_id & 0xFFFF);
> + drv_info = cd->cldma_drv_info[que->hif_id];
> + if (unlikely(!drv_info))
> + return -EPIPE;
[Severity: Medium]
Does this early return leak the disable request? Unlike the success path
below, this branch never sets trb->status nor calls
trb->trb_complete(skb), and mtk_ctrl_trb_handler() has already unlinked
the SKB and ignores the return value of mtk_cldma_trb_process().
The submitter is left waiting on the completion:
mtk_port.c:mtk_port_ch_disable() {
ret = wait_event_timeout(port->trb_wq, trb->status <= 0,
MTK_DFLT_TRB_TIMEOUT);
if (!ret)
ret = -ETIMEDOUT;
...
}
so the close path stalls for the full timeout, and since trb_complete() is
where the last reference is dropped, is the 3.5 KiB command SKB leaked
too? This looks reachable after mtk_cldma_dev_exit() has NULLed
cldma_drv_info[] on modem off, with a userspace close arriving afterwards.
> +
> + if (drv_info->txq[que->txqno])
> + mtk_cldma_txq_free(drv_info, que->txqno);
> + if (drv_info->rxq[que->rxqno])
> + mtk_cldma_rxq_free(drv_info, que->rxqno);
> +
> + trb->status = 0;
> + trb->trb_complete(skb);
> +
> + return 0;
> +}
[ ... ]
> + } else {
> + req->data_dma_addr = dma_map_single(mdev->dev, skb->data,
> + skb_headlen(skb), DMA_TO_DEVICE);
> + ret = dma_mapping_error(mdev->dev, req->data_dma_addr);
> + if (unlikely(ret)) {
> + req->data_dma_addr = 0;
> + goto err_exit;
> + }
[ ... ]
> +err_exit:
> + dev_err(mdev->dev, "Failed to map dma! error:%d\n", ret);
> + return -EAGAIN;
> +}
[Severity: Medium]
Can a persistent mapping failure spin the TRB service thread at 100% CPU?
A mapping failure is reported with the same -EAGAIN used for an exhausted
descriptor ring, and mtk_cldma_submit_tx() returns before it consumes any
budget, so the ring-full backpressure does not apply:
mtk_trans_ctrl.c:mtk_ctrl_trb_handler() {
case TRB_CMD_TX:
err = mtk_cldma_submit_tx(trans->dev, skb);
if (err) {
...
if (err == -EAGAIN)
return;
...
}
The SKB stays queued and req_budget stays positive, so:
mtk_ctrl_chs_is_busy_or_empty() {
if (!skb_queue_empty(...) &&
mtk_cldma_get_tx_budget(srv->trans->dev, i, srv_que->qno))
return false;
...
}
reports work available and the thread's inner loop reruns immediately:
do {
mtk_ctrl_trb_process(srv);
cond_resched();
} while (!mtk_ctrl_chs_is_busy_or_empty(srv) && ...);
Would that also flood the log, given dev_err() is unconditional on every
retry? Should the mapping failure use a distinct error and complete the
request, or add some backoff?
> +
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
> new file mode 100644
> index 0000000000000..55297cfcdbc61
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c
> @@ -0,0 +1,541 @@
[ ... ]
> +static struct mtk_ctrl_info_desc mtk_ctrl_info_tbl[] = {
> + {2304, &ctrl_info_name(m9xx)},
> + {0, NULL},
> +};
[Severity: Medium]
Does this table cover both PCI IDs the driver claims? t9xx_pci_table[]
lists both:
MTK_PCI_DEV_CFG(0x0900, mtk_dev_cfg_0900),
CEI_PCI_DEV_CFG(0x01CA, mtk_dev_cfg_0900),
but this table holds only 2304 (0x0900), so a 0x01CA device finds no
match, trans->queue_info stays NULL and mtk_trans_ctrl_init() aborts with
"Failed to get ctrl info!".
The mismatch also persists to the end of the series with the IDs inverted:
mtk_ctrl_info_tbl[] and cldma_drv_info_tbl[] are then keyed on 0x01CA only,
while t9xx_pci_table[] still claims both, so the 0x0900 device becomes the
one that can never bind. Since both entries share mtk_dev_cfg_0900, is
the omission intentional?
> +
> +#define QUEUE_CHL_MASK 0xFFFF
> +
> +static bool mtk_queue_list_is_full(struct mtk_ctrl_trans *trans, struct queue_info *que)
> +{
> + return trans->trans_list[que->hif_id].skb_list[que->txqno].qlen >= SKB_LIST_MAX_LEN;
> +}
[Severity: Medium]
Is qlen safe to read here without the queue lock, and is the depth limit
actually enforced? In mtk_pcie_hif_submit_skb() the fullness test and the
insertion are separate critical sections:
if (mtk_queue_list_is_full(trans, que) && !force_send)
return -EAGAIN;
...
if (trb->cmd == TRB_CMD_DISABLE)
skb_queue_head(&trans->trans_list[que->hif_id].skb_list[que->txqno], skb);
else
skb_queue_tail(&trans->trans_list[que->hif_id].skb_list[que->txqno], skb);
so concurrent submitters on the same hardware queue can all pass the test
and push past SKB_LIST_MAX_LEN.
Separately, mtk_ctrl_trb_handler() walks the list with skb_peek(),
skb_queue_is_last() and skb_peek_next() outside the lock, and reads a
freshly published SKB's control block with no acquire barrier:
skb_next = skb_peek_next(skb, skb_list);
trb_next = (struct trb *)skb_next->cb;
if (trb_next->cmd != TRB_CMD_TX)
kick = true;
Can that observe stale ->cb contents on a weakly ordered CPU and
mis-decide whether to kick the hardware? Multiple ports share one
hardware queue, so concurrent producers plus this single consumer look
like normal operation.
[ ... ]
> +static int mtk_pcie_hif_exit(struct mtk_md_dev *mdev)
> +{
> + struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
> + struct mtk_ctrl_trans *trans;
> +
> + trans = ctrl_blk->ctrl_hw_priv;
> +
> + atomic_set(&trans->available, 0);
> + mtk_ctrl_trb_srv_exit(trans);
> + mtk_ctrl_remove_radix_tree(trans);
> + mtk_cldma_exit(trans);
> +
> + return 0;
> +}
> +
> +static int mtk_pcie_hif_submit_skb(struct mtk_md_dev *mdev, struct sk_buff *skb, bool force_send)
> +{
[ ... ]
> + que = radix_tree_lookup(&trans->queue_tbl, trb->channel_id & QUEUE_CHL_MASK);
> + if (!que) {
> + dev_warn(mdev->dev, "lookup que fail, ch_id: %x, que: 0x%p\n",
> + trb->channel_id, que);
> + return -EINVAL;
> + }
> +
> + if (!atomic_read(&trans->available))
> + return -EIO;
[Severity: High]
Does this atomic actually provide mutual exclusion against teardown? It
looks like a state flag rather than a lock, and the objects it is guarding
are freed immediately after it is cleared:
mtk_pcie_hif_exit() {
atomic_set(&trans->available, 0);
mtk_ctrl_trb_srv_exit(trans); /* kthread_stop, kfree(srv), trb_srv[i] = NULL */
mtk_ctrl_remove_radix_tree(trans); /* radix_tree_delete + kfree(queue) */
mtk_cldma_exit(trans);
}
The radix-tree lookup above is also lockless with no RCU read-side
section, while mtk_ctrl_remove_radix_tree() deletes and kfree()s each
entry with no grace period:
radix_tree_delete(&trans->queue_tbl, iter.index);
kfree(queue);
So can this sequence occur?
CPU0 (write/close on a WWAN port)
mtk_pcie_hif_submit_skb()
que = radix_tree_lookup(...); /* que valid */
atomic_read(&trans->available); /* 1 */
<preempted>
CPU1 (FSM thread on FSM_STATE_OFF)
mtk_pcie_hif_exit()
atomic_set(&trans->available, 0);
kfree(srv); trans->trb_srv[i] = NULL;
kfree(queue);
CPU0 resumes and uses the freed que, then:
wake_up(&trans->trb_srv[trans->srv_cfg[que->hif_id][que->txqno]]->trb_waitq);
which dereferences a NULL or freed trb_srv. The SKB is also stranded
because the service thread has already run mtk_ctrl_chs_flush() and
exited. Would a mutex or RCU around the lookup and submit be more
appropriate than the flag?
> +
> + if (mtk_queue_list_is_full(trans, que) && !force_send)
> + return -EAGAIN;
[ ... ]
> +static int mtk_pcie_hif_cmd_func(struct mtk_md_dev *mdev, int cmd, void *data)
> +{
> + struct mtk_ctrl_blk *ctrl_blk = mdev->ctrl_blk;
> + struct mtk_ctrl_trans *trans;
> + struct queue_info *que;
> +
> + switch (cmd) {
> + case HIF_CTRL_CMD_CHECK_TX_FULL:
> + trans = ctrl_blk->ctrl_hw_priv;
> + que = radix_tree_lookup(&trans->queue_tbl,
> + ((union ctrl_hif_cmd_data *)data)->rx_ch & QUEUE_CHL_MASK);
> + if (!que) {
> + dev_warn(mdev->dev, "Failed to find que to check tx full\n");
> + return -EINVAL;
> + }
> + return mtk_queue_list_is_full(trans, que);
This path does the same lockless lookup and dereference without even
testing trans->available. Is that intentional?
> + default:
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
[ ... ]
The remaining items below were also raised during review but appear to be
resolved by later patches in this series, or could not be shown to be
reachable. They are listed for completeness only.
[Severity: Low]
At this commit, mtk_ctrl_cfg_m9xx.c defines an empty table:
static const struct queue_info mtk_queue_info_m9xx[] = {
};
with .queue_info_num = ARRAY_SIZE(mtk_queue_info_m9xx), so queue_info_num
is 0 and mtk_trans_ctrl_init() rejects it via "trans->queue_info_num <= 0",
failing probe for every device. The later patch "net: wwan: t9xx: Add
control port" populates four entries, so this is only a transient
bisect-window concern - was that ordering deliberate?
[Severity: Low]
None of the new control-plane and CLDMA code is reachable at this commit:
mtk_ctrl_init() only stores ops, mtk_ctrl_exit() only NULLs
mdev->ctrl_blk, mtk_pci_dev_start() is a bare return 0, and nothing
assigns cd->cldma_drv_info[], so there are no DMA pools, no base_addr, no
IRQ handler and the tx_done_work/rx_done_work items are never queued. The
TX and RX flows described in the commit message start working only after
the later "Add control port" and "Add FSM thread" patches. Could the
commit message note that the paths are wired up later in the series?
[Severity: Low]
Related to the above: at this commit mtk_trans_ctrl_exit() calls only
mtk_ctrl_exit(), which never invokes ops->exit, so mtk_pcie_hif_exit() has
no caller and the radix-tree entries, cldma_dev, trb_srv/srv_que objects
and TRB kthreads would have no release path. This is vacuous here since
ops->init is not called either, and the FSM teardown added later closes it.
[Severity: Low]
In mtk_ctrl_trb_handler(), the default case unlinks the SKB without
completing or freeing it, and kick stays false:
default:
skb_unlink(skb, skb_list);
}
No in-tree producer can reach it, since mtk_pcie_hif_submit_skb()
completes TRB_CMD_STOP and TRB_CMD_RECOVER without queueing them and the
only other values assigned to trb->cmd are ENABLE, TX and DISABLE. Would
completing the request there be worth it as hardening?
[Severity: Low]
In the non-BD TX path, mtk_cldma_txbuf_set() maps skb_headlen(skb) while
mtk_cldma_submit_tx() advertises data_buff_len = skb->len and records
req->data_len = skb->len for the unmap. These agree today because non-BD
mode is chosen only when que->tx_mtu <= que->tx_frag_size, and the port
layer builds a frag_list only when port->tx_mtu > port->tx_frag_size, so
every non-BD SKB is linear. Would an explicit skb_is_nonlinear() check or
a comment make the invariant clearer?
[Severity: Low]
skb->data is overlaid with struct trb_open_priv in mtk_ch_status_check()
and mtk_cldma_rxq_alloc() (including loading the rx_done function pointer
that is later called) with no length check on the linear region. The only
producer, mtk_port_ch_enable(), allocates Q_MTU_3_5K and skb_put()s
sizeof(struct trb_open_priv), so there is enough room in practice.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread
2026-08-11 7:14 ` [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
@ 2026-08-17 22:36 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:36 UTC (permalink / raw)
To: jackbb_wu
Cc: Jakub Kicinski, loic.poulain, ryazanov.s.a, johannes,
andrew+netdev, davem, edumazet, pabeni, wen-zhi.huang,
shi-wei.yeh, Minano.tseng, matthias.bgg,
angelogioacchino.delregno, horms, corbet, skhan, robert_yu,
Jeff_Chang, linux-kernel, netdev, linux-arm-kernel,
linux-mediatek, linux-doc
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add FSM thread
This adds a finite-state-machine kthread that serializes asynchronous
events from the device and the OS, plus a notifier mechanism so other
modules can react before and after each state transition. It also wires
the FSM into the PCI probe/remove path and into the control plane and
CLDMA transport.
No Link: tags are present in the commit header.
> net: wwan: t9xx: Add FSM thread
>
> The FSM (Finite-state Machine) thread is responsible for
> synchronizing the actions of different modules. The
> asynchronous events from the device or the OS will trigger
> a state transition.
>
> The FSM thread will append it to the event queue when an
> event arrives. It handles the events sequentially. After
> processing the event, the FSM thread notifies other modules
> before and after the state transition.
>
> Seven FSM states are defined. They can transition from one
> state to another, self-transition in some states, and
> transition in some sub-states.
[Severity: Low]
Does the code match this description? enum mtk_fsm_state in the new
mtk_fsm.h defines five states:
FSM_STATE_INVALID, FSM_STATE_OFF, FSM_STATE_ON,
FSM_STATE_BOOTUP, FSM_STATE_READY
There is also no sub-state anywhere: struct mtk_md_fsm and
struct mtk_fsm_param have no sub-state member, and mtk_fsm_switch_state()
only ever assigns fsm->state. Should the count and the sub-state
sentence be updated?
[Severity: Low]
The changelog only mentions the FSM thread, but the patch carries several
other functional changes. Would it help reviewers to split them out or at
least mention them?
- the HS1/HS2/HS3 handshake and runtime-feature protocol
(mtk_fsm_send_hs1_msg(), mtk_fsm_parse_hs2_msg(),
mtk_fsm_send_hs3_msg() and the two ctrl-msg handlers)
- the whole operational CLDMA bring-up and teardown path
(cldma_drv_info_tbl, mtk_cldma_get_drv_info(), mtk_cldma_isr(),
mtk_cldma_dev_init(), mtk_cldma_dev_exit(), including DMA pools,
a workqueue and MSI-X registration)
- an skb leak fix in mtk_cldma_rxq_free(), adding
dev_kfree_skb_any(req->skb) in the rxq->nr_bds branch to code added
by the earlier CLDMA patch of this series, with no Fixes: tag
- removal of all declared CLDMA4 support (CLDMA4 from enum mtk_hif_id,
which shrinks NR_CLDMA from 3 to 2, plus CLDMA4_HW_ID,
CLDMA4_BASE_ADDR, REG_CLDMA_INT_WF_MASK, the cldma4_base_addr and
reg_cldma4_int_mask fields, and the CLDMA4 branch of
mtk_cldma_drv_init_m9xx())
> diff --git a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> index 6ed47d0745565..1d9dc53482405 100644
> --- a/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> +++ b/drivers/net/wwan/t9xx/mtk_ctrl_plane.c
> @@ -5,10 +5,46 @@
> +static void mtk_ctrl_trans_fsm_state_handler(struct mtk_fsm_param *param,
> + struct mtk_ctrl_blk *ctrl_blk)
> +{
> + struct mtk_md_dev *mdev = ctrl_blk->mdev;
> +
> + switch (param->to) {
> + case FSM_STATE_OFF:
> + ctrl_blk->ops->fsm_indication(mdev, param);
> + ctrl_blk->ops->exit(mdev);
> + break;
> + case FSM_STATE_ON:
> + ctrl_blk->ops->init(mdev);
> + fallthrough;
[Severity: High]
Can a failing ops->init() here lead to a NULL dereference on removal?
ops->init is mtk_pcie_hif_init(), which has several failure exits (kmemdup
ENOMEM, queue-info validation, radix_tree_insert(), mtk_cldma_init(),
mtk_ctrl_trb_srv_init()). In all of them trans->trb_srv[] is left
all-NULL, but the return value is discarded here and the FSM commits
FSM_STATE_ON anyway.
On removal the FSM_STATE_OFF arm above calls ops->exit()
unconditionally:
mtk_pci_dev_exit() -> FSM_EVT_DEV_RM -> mtk_fsm_dev_rm_act()
-> mtk_fsm_enter_off_state() (guard passes, state == ON)
-> mtk_fsm_switch_state(FSM_STATE_OFF)
-> mtk_ctrl_trans_fsm_state_handler() case FSM_STATE_OFF
-> mtk_pcie_hif_exit() -> mtk_ctrl_trb_srv_exit()
and mtk_ctrl_trb_srv_exit() does:
for (i = 0; i < trans->trb_srv_num; i++) {
srv = trans->trb_srv[i];
kthread_stop(srv->trb_thread);
with no NULL check. The same unchecked-return pattern repeats one level
down, where the void mtk_cldma_fsm_state_listener() discards
mtk_cldma_dev_init() errors.
> + default:
> + ctrl_blk->ops->fsm_indication(mdev, param);
> + break;
> + }
> +}
> +
> +static void mtk_ctrl_fsm_state_listener(struct mtk_fsm_param *param, void *data)
> +{
> + struct mtk_ctrl_blk *ctrl_blk = data;
> +
> + mtk_port_mngr_fsm_state_handler(param, ctrl_blk->port_mngr);
> + mtk_ctrl_trans_fsm_state_handler(param, ctrl_blk);
> + mtk_port_mngr_fsm_state_handler_late(param, ctrl_blk->port_mngr);
> +}
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/mtk_fsm.c b/drivers/net/wwan/t9xx/mtk_fsm.c
> new file mode 100644
> index 0000000000000..8cf9b239caeaf
> --- /dev/null
> +++ b/drivers/net/wwan/t9xx/mtk_fsm.c
[ ... ]
> +static int mtk_fsm_parse_hs2_msg(struct fsm_hs_info *hs_info)
> +{
> + struct mtk_md_fsm *fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
> + char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
> + enum runtime_feature_support_type cur_ft_spt;
> + struct runtime_feature_entry *rtft_entry;
> + unsigned int ft_id, offset, data_len;
> + int ret = 0;
> +
> + offset = sizeof(struct feature_query);
> + for (ft_id = 0; ft_id < FEATURE_CNT; ft_id++) {
> + if (offset + sizeof(*rtft_entry) > hs_info->rt_data_len)
> + break;
> +
> + rtft_entry = (struct runtime_feature_entry *)(rt_data + offset);
> + ret = mtk_fsm_feature_set_match(&cur_ft_spt,
> + rtft_entry->support_info,
> + hs_info->query_ft_set[ft_id]);
[Severity: Low]
Should this loop key off rtft_entry->feature_id instead of the loop index?
The wire structure carries an explicit id:
struct runtime_feature_entry {
u8 feature_id;
struct runtime_feature_info support_info;
and the host's own emitter fills it in mtk_fsm_append_rtft_entries():
rtft_entry->feature_id = ft_id;
Here the received entry's feature_id is never read, so the parser assumes
the device returns exactly FEATURE_CNT entries in dense ascending order.
If entries are omitted or reordered, one feature's payload is handed to
another feature's handler, or mtk_fsm_feature_set_match() returns -EPROTO
for a feature the device did answer.
> + if (ret < 0)
> + break;
> +
> + data_len = le32_to_cpu(rtft_entry->data_len);
> + if (data_len > hs_info->rt_data_len - offset - sizeof(*rtft_entry))
> + break;
> +
> + if (cur_ft_spt == RTFT_TYPE_MUST_SUPPORT)
> + if (query_rtft_action[ft_id])
> + ret = query_rtft_action[ft_id](fsm->mdev,
> + rtft_entry->data,
> + data_len);
[Severity: Medium]
Is a minimum length check missing here? Only an upper bound on data_len
is applied, so data_len == 0 is accepted and passed to
mtk_port_status_update(), which dereferences the 12-byte
struct mtk_port_enum_msg header before validating the length:
drivers/net/wwan/t9xx/mtk_port.c:mtk_port_status_update() {
if (le16_to_cpu(msg->version) != MTK_PORT_ENUM_VER ||
le32_to_cpu(msg->head_pattern) != MTK_PORT_ENUM_HEAD_PATTERN ||
le32_to_cpu(msg->tail_pattern) != MTK_PORT_ENUM_TAIL_PATTERN)
return -EPROTO;
if (data_len < sizeof(*msg) + ...
}
With data_len == 0 for QUERY_RTFT_ID_MD_PORT_ENUM or
QUERY_RTFT_ID_SAP_PORT_ENUM (both declared MUST-support by the host, so
cur_ft_spt == RTFT_TYPE_MUST_SUPPORT is reachable), rtft_entry->data
equals rt_data + rt_data_len and the read goes past the received message
into skb tailroom / skb_shared_info. Can the handler be given a
guaranteed-minimum length instead?
> + if (ret < 0)
> + break;
> +
> + offset += sizeof(*rtft_entry) + data_len;
> + }
[ ... ]
> +static int mtk_fsm_sap_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
> +{
[ ... ]
> + hs_info->rt_data = skb;
> + hs_info->rt_data_len = skb->len;
> + ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
> + if (ret == FSM_EVT_RET_FAIL) {
> + dev_kfree_skb(skb);
> + hs_info->rt_data = NULL;
> + }
> +
> + return 0;
> +}
> +
> +static int mtk_fsm_md_ctrl_msg_handler(void *__fsm, struct sk_buff *skb)
> +{
[ ... ]
> + switch (le32_to_cpu(ctrl_msg_h->id)) {
> + case CTRL_MSG_HS2:
> + skb_pull(skb, sizeof(*ctrl_msg_h));
> + hs_info->rt_data = skb;
> + hs_info->rt_data_len = skb->len;
> + ret = mtk_fsm_evt_submit(fsm->mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs2, hs_info, sizeof(*hs_info), 0);
> + if (ret != FSM_EVT_RET_FAIL)
> + consumed_skb = true;
> + break;
[Severity: High]
Can two HS2 messages race on the single hs_info->rt_data slot?
The queued event carries only the hs_info pointer (event->data = hs_info),
never the skb, so no event owns its buffer. The writer here runs in the
CLDMA rx_done work while the reader/freer runs in the FSM kthread, and
neither takes a lock or checks whether a buffer is already pending.
If the modem sends a second CTRL_MSG_HS2 while the FSM thread is still in
mtk_fsm_startup_act() for the first one:
rx work FSM kthread
hs_info->rt_data = skb2 mtk_fsm_parse_hs2_msg() /* skb1 */
submit event B
dev_kfree_skb(hs_info->rt_data);
hs_info->rt_data = NULL;
skb1 is leaked, skb2 is freed early, and event B then reaches:
char *rt_data = ((struct sk_buff *)hs_info->rt_data)->data;
on a NULL pointer. In the failure branches above, dev_kfree_skb(skb) can
also free a buffer the FSM thread is parsing right now, and a torn
rt_data / rt_data_len pair makes the parser bound the wrong buffer.
Should the skb be handed to the event itself rather than stored in the
shared hs_info?
> + default:
> + dev_err(fsm->mdev->dev, "Invalid ctrl msg id\n");
> + }
[ ... ]
> +static void mtk_fsm_switch_state(struct mtk_md_fsm *fsm,
> + enum mtk_fsm_state to_state, struct mtk_fsm_evt *event)
> +{
[ ... ]
> + list_for_each_entry(nt, &fsm->pre_notifiers, entry)
> + nt->cb(¶m, nt->data);
> +
> + fsm->state = to_state;
> + fsm->fsm_flag |= event ? event->fsm_flag : FSM_F_DFLT;
[ ... ]
> + list_for_each_entry(nt, &fsm->post_notifiers, entry)
> + nt->cb(¶m, nt->data);
> +}
[Severity: High]
What protects these two list walks against concurrent unregistration?
The traversal runs in the FSM kthread with plain list_for_each_entry() and
calls nt->cb(), while mtk_fsm_notifier_register() /
mtk_fsm_notifier_unregister() do list_add()/list_add_tail()/list_del() and
kfree() from other threads. There is no mutex, no _rcu list variant, no
synchronize_rcu() before the kfree, and no barrier.
The removal path is ordered against the FSM thread only by the blocking
FSM_EVT_DEV_RM event, whose result mtk_pci_dev_exit() ignores and which
can also time out. When that happens:
mtk_trans_ctrl_exit() -> mtk_ctrl_exit()
-> mtk_fsm_notifier_unregister(mdev, MTK_USER_CTRL) /* list_del + kfree */
-> mtk_port_mngr_exit(ctrl_blk) /* frees port_mngr */
while the FSM thread may still be iterating post_notifiers and about to
call nt->cb() on the freed notifier with the freed port_mngr as data.
Since both register and unregister are exported, any future runtime
register/unregister races with every state transition too.
> +static int mtk_fsm_startup_act(struct mtk_md_fsm *fsm, struct mtk_fsm_evt *event)
> +{
[ ... ]
> + } else if (event->fsm_flag & FSM_HS2_DONE_MASK) {
> + ret = mtk_fsm_parse_hs2_msg(hs_info);
> + if (!ret) {
> + mtk_fsm_switch_state(fsm, to_state, event);
> + ret = mtk_fsm_send_hs3_msg(hs_info);
> + }
> + dev_kfree_skb(hs_info->rt_data);
> + hs_info->rt_data = NULL;
> + if (ret)
> + goto hs_err;
> + }
> +
> + if (((fsm->fsm_flag | event->fsm_flag) & fsm->hs_done_flag) == fsm->hs_done_flag) {
> + to_state = FSM_STATE_READY;
> + mtk_fsm_switch_state(fsm, to_state, NULL);
> + }
[Severity: Medium]
Can the FSM reach FSM_STATE_READY when HS3 was never delivered?
mtk_fsm_switch_state() is called before mtk_fsm_send_hs3_msg(), and it
permanently ORs the event flag in:
fsm->fsm_flag |= event ? event->fsm_flag : FSM_F_DFLT;
If mtk_fsm_send_hs3_msg() then fails (-ENOMEM from
__dev_alloc_skb(RTFT_DATA_SIZE), -EPROTO from
mtk_fsm_append_rtft_entries(), or a failed mtk_port_internal_write()), the
hs_err exit does not clear the already-committed FSM_F_*_HS2_DONE bit.
In the dual-handshake configuration the other side can then complete, and
the hs_done_flag test above sees every HS_START/HS2_DONE bit set and moves
to FSM_STATE_READY.
[ ... ]
> +static int mtk_fsm_hs1_handler(u32 status, void *__hs_info)
> +{
> + struct fsm_hs_info *hs_info = __hs_info;
> + struct mtk_md_dev *mdev;
> + struct mtk_md_fsm *fsm;
> +
> + fsm = container_of(hs_info, struct mtk_md_fsm, hs_info[hs_info->id]);
> + mdev = fsm->mdev;
> + mtk_fsm_evt_submit(mdev, FSM_EVT_STARTUP,
> + hs_info->fsm_flag_hs1, hs_info, sizeof(*hs_info), 0);
> + mtk_dev_mask_dev_evt(mdev, hs_info->mhccif_ch);
> + mtk_dev_clear_dev_evt(mdev, hs_info->mhccif_ch);
> +
> + return 0;
> +}
[Severity: Medium]
Is there a way back if the queued startup action later fails?
The HS1 notification is masked and cleared here, before the asynchronous
event result is known. In mtk_fsm_startup_act():
ret = mtk_fsm_ctrl_ch_start(fsm, hs_info, O_NONBLOCK);
if (!ret)
ret = mtk_fsm_send_hs1_msg(hs_info);
if (ret)
goto hs_err;
mtk_fsm_ctrl_ch_start() returns -ENODEV when the control port is not
enabled (which is what happens when mtk_cldma_dev_init() failed, since
mtk_cldma_fsm_state_listener() drops that error), and
mtk_fsm_send_hs1_msg() can return -ENOMEM or a write error. The hs_err
path only logs; the channel stays masked and no retry is scheduled, so the
FSM stays in FSM_STATE_BOOTUP. The only mtk_dev_unmask_dev_evt() calls
for hs_info->mhccif_ch are in mtk_fsm_idle_evt_handler(), which is not
re-entered because the boot-flow-sync channel is masked as well.
[ ... ]
> +static void mtk_fsm_notifier_insert(struct mtk_fsm_notifier *notifier, struct list_head *head)
> +{
> + struct mtk_fsm_notifier *nt;
> +
> + list_for_each_entry(nt, head, entry) {
> + if (notifier->prio > nt->prio) {
> + list_add(¬ifier->entry, nt->entry.prev);
> + return;
> + }
> + }
> + list_add_tail(¬ifier->entry, head);
> +}
[ ... ]
> + spin_lock_irqsave(&fsm->evtq_lock, flags);
> + if (test_bit(EVT_TF_GATECLOSED, &fsm->t_flag)) {
> + spin_unlock_irqrestore(&fsm->evtq_lock, flags);
> + mtk_fsm_evt_put(event);
> + dev_err(mdev->dev, "Failed to add event, fsm dev has been removed!\n");
> + return FSM_EVT_RET_FAIL;
> + }
> +
> + kref_get(&event->kref);
> + if (mode & EVT_MODE_TOHEAD)
> + list_add(&event->entry, &fsm->evtq);
> + else
> + list_add_tail(&event->entry, &fsm->evtq);
> + wake_up_process(fsm->fsm_handler);
> + spin_unlock_irqrestore(&fsm->evtq_lock, flags);
[Severity: High]
Can fsm->fsm_handler be NULL at this wake_up_process()?
mtk_fsm_exit() clears it under the same lock:
spin_lock_irqsave(&fsm->evtq_lock, flags);
handler = fsm->fsm_handler;
fsm->fsm_handler = NULL;
spin_unlock_irqrestore(&fsm->evtq_lock, flags);
if (handler)
kthread_stop(handler);
and only unregisters mtk_fsm_early_bootup_handler() /
mtk_fsm_hs1_handler() at the very end, after a kthread_stop() that can
block while the FSM thread finishes an event. The only thing stopping a
submit in that window is EVT_TF_GATECLOSED, which is set exclusively in
mtk_fsm_dev_rm_act(), i.e. only if a FSM_EVT_DEV_RM was queued and
processed. On the probe error path (mtk_pci_dev_init() -> goto free_fsm
-> mtk_fsm_exit()) no DEV_RM is ever submitted, so the gate is open, and
on removal mtk_pci_dev_exit() ignores the DEV_RM result.
Any device event arriving then (mtk_mhccif_isr_work() ->
mtk_fsm_early_bootup_handler() -> mtk_fsm_idle_evt_handler() ->
mtk_fsm_evt_submit(), or mtk_fsm_hs1_handler()) passes a NULL task_struct
into try_to_wake_up(). mtk_fsm_start() already has the check this path is
missing:
if (!fsm->fsm_handler)
return -EFAULT;
mtk_fsm_exit() also never clears mdev->fsm, so the torn-down (devm-freed)
fsm stays reachable from those callbacks.
[ ... ]
> +int mtk_fsm_init(struct mtk_md_dev *mdev)
> +{
[ ... ]
> + mtk_fsm_hs_info_init(fsm);
> + mtk_dev_register_dev_evt(mdev, DEV_EVT_D2H_BOOT_FLOW_SYNC,
> + mtk_fsm_early_bootup_handler, fsm);
> + mdev->fsm = fsm;
> + return 0;
> +exit:
> + return ret;
> +}
[Severity: Medium]
Should these registrations be checked? mtk_dev_register_dev_evt() reaches
mtk_pci_register_ext_evt(), which returns -EINVAL for bad arguments,
-EFAULT when the channel bitmask intersects an existing registration, and
-ENOMEM from:
cb = kzalloc(sizeof(*cb), GFP_ATOMIC);
if (!cb) {
ret = -ENOMEM;
goto err_spin_unlock;
}
Here the return value is dropped, and the two per-handshake-channel
registrations inside the void mtk_fsm_hs_info_init() cannot report failure
at all. If the BOOT_FLOW_SYNC registration fails,
mtk_fsm_early_bootup_handler() never runs, so
mtk_fsm_idle_evt_handler() never sets fsm->hs_done_flag and never submits
the STARTUP event: probe succeeds, the FSM never leaves FSM_STATE_ON, no
CLDMA is initialized, no port is created, and nothing is logged.
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> index 097b20d8fbd38..1431c2797eb5d 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_cldma.c
> @@ -34,12 +34,172 @@
[ ... ]
> +static int mtk_cldma_isr(int irq_id, void *param)
> +{
> + struct cldma_drv_info *drv_info = param;
> + struct mtk_md_dev *mdev;
> + u32 tx_done, rx_done;
> + u32 tx_sta, rx_sta;
> + struct txq *txq;
> + struct rxq *rxq;
> + int i;
> +
> + mdev = drv_info->mdev;
> + drv_info->drv_ops->cldma_get_intr_status(drv_info, &tx_sta, &rx_sta);
> + tx_done = (tx_sta >> QUEUE_XFER_DONE) & 0xFF;
> + rx_done = (rx_sta >> QUEUE_XFER_DONE) & 0xFF;
[Severity: Medium]
What happens to the QUEUE_ERROR bits that get masked but never handled?
mtk_cldma_get_intr_status() masks every asserted bit but clears only the
XFER_DONE byte:
drivers/net/wwan/t9xx/pcie/mtk_cldma_drv.c:mtk_cldma_get_intr_status() {
if (*tx_sta) {
/* TX XFER_DONE and QUEUE_ERROR mask */
mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2timsr0, *tx_sta);
/* TX XFER_DONE clear */
mtk_pci_write32(mdev, base + hw_regs->reg_cldma_l2tisar0,
(*tx_sta) & (0xFF << QUEUE_XFER_DONE));
}
}
The two shifts above then drop QUEUE_ERROR and QUEUE_ACTIVE_START
entirely. QUEUE_ERROR is unmasked only once, in mtk_cldma_txq_alloc() /
mtk_cldma_rxq_alloc(); the done workers re-unmask only QUEUE_XFER_DONE.
So the first queue error permanently disarms error reporting, leaves the
bit latched in L2TISAR0/L2RISAR0, and leaves the halted queue with no log,
no queue reset and no GPD/HWO recovery.
> +
> + if (tx_done) {
> + for (i = 0; i < HW_QUEUE_NUM; i++) {
> + txq = drv_info->txq[i];
> + if (!(tx_done & BIT(i)) || !txq)
> + continue;
> + queue_work(drv_info->wq, &txq->tx_done_work);
> + }
> + }
[ ... ]
> + mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
> + mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
> +
> + return IRQ_HANDLED;
> +}
[ ... ]
> @@ -885,6 +1046,44 @@ static void mtk_cldma_rxq_free(struct cldma_drv_info *drv_info, u32 rxqno)
> +static int mtk_cldma_dev_exit(struct cldma_dev *cd, int hif_id)
> +{
[ ... ]
> + virq_id = mtk_pci_get_virq_id(mdev, drv_info->pci_ext_irq_id);
> + mtk_pci_mask_irq(mdev, drv_info->pci_ext_irq_id);
> + synchronize_irq(virq_id);
> + for (i = 0; i < HW_QUEUE_NUM; i++) {
> + if (drv_info->txq[i])
> + mtk_cldma_txq_free(drv_info, drv_info->txq[i]->txqno);
> + if (drv_info->rxq[i])
> + mtk_cldma_rxq_free(drv_info, drv_info->rxq[i]->rxqno);
> + }
> +
> + flush_workqueue(drv_info->wq);
> + destroy_workqueue(drv_info->wq);
> + dma_pool_destroy(drv_info->bd_dma_pool);
> + dma_pool_destroy(drv_info->gpd_dma_pool);
> + mtk_pci_unregister_irq(mdev, drv_info->pci_ext_irq_id);
> +
> + kfree(drv_info);
> + cd->cldma_drv_info[hif_id] = NULL;
> +
> + return 0;
> +}
[Severity: High]
Is the initial mask plus synchronize_irq() enough to keep the ISR away
from the freed drv_info?
mtk_cldma_isr() re-arms its own vector on every exit:
mtk_pci_clear_irq(mdev, drv_info->pci_ext_irq_id);
mtk_pci_unmask_irq(mdev, drv_info->pci_ext_irq_id);
so an ISR that was already running when the vector was masked unmasks it
again before returning, and synchronize_irq() only waits for that ISR to
finish. QUEUE_ERROR stays armed in hardware, so a later interrupt can
call mtk_cldma_isr() after the workqueue is destroyed (queue_work() on
freed workqueue memory) or after kfree(drv_info)
(drv_info->drv_ops->cldma_get_intr_status(), drv_info->txq[]).
mtk_pci_unregister_irq() only clears the callback pointer:
priv->irq_cb_list[irq_id] = NULL;
priv->irq_cb_data[irq_id] = NULL;
and is not followed by a synchronize_irq(), while mtk_pci_irq_handler()
invokes priv->irq_cb_list[irq_id] directly from hardirq with no lock or
RCU. Should the callback be unregistered and synchronized before the
workqueue, pools and drv_info are released?
[ ... ]
> @@ -1195,6 +1394,27 @@ int mtk_cldma_trb_process(void *dev, struct sk_buff *skb)
> +void mtk_cldma_fsm_state_listener(struct mtk_fsm_param *param, struct mtk_ctrl_trans *trans)
> +{
> + struct cldma_dev *cd = trans->dev;
> + int i;
> +
> + switch (param->to) {
> + case FSM_STATE_BOOTUP:
> + if (param->fsm_flag & FSM_F_SAP_HS_START)
> + mtk_cldma_dev_init(cd, CLDMA0);
> + else if (param->fsm_flag & FSM_F_MD_HS_START)
> + mtk_cldma_dev_init(cd, CLDMA1);
> + break;
> + case FSM_STATE_OFF:
> + for (i = 0; i < NR_CLDMA; i++)
> + mtk_cldma_dev_exit(cd, i);
> + break;
[Severity: High]
Are the TRB service threads guaranteed to be stopped before this runs?
In mtk_ctrl_trans_fsm_state_handler(), the FSM_STATE_OFF arm calls
fsm_indication() (which lands here and tears down every CLDMA hif)
before ops->exit(), and it is ops->exit() -> mtk_pcie_hif_exit() that
clears trans->available and stops the TRB kthreads. Meanwhile
mtk_cldma_submit_tx() reads the published pointers locklessly:
drv_info = cd->cldma_drv_info[que->hif_id];
if (unlikely(!drv_info))
return -EINVAL;
txq = drv_info->txq[que->txqno];
and mtk_cldma_dev_exit() does kfree(drv_info) before clearing
cd->cldma_drv_info[hif_id], so the pointer is dangling across the free.
One way to have work still in flight: mtk_port_ch_disable()'s
wait_event_timeout() can expire, leaving the DISABLE TRB queued for the
service thread to pick up later.
> + default:
> + break;
> + }
> +}
[ ... ]
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_pci.c b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> index dbc17da590bc0..bf4e110b485c8 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_pci.c
> @@ -883,22 +883,34 @@ static int mtk_pci_dev_init(struct mtk_md_dev *mdev)
[ ... ]
> static void mtk_pci_dev_exit(struct mtk_md_dev *mdev)
> {
> + mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_RM, 0, NULL, 0,
> + EVT_MODE_BLOCKING | EVT_MODE_TOHEAD);
> mtk_trans_ctrl_exit(mdev);
> + mtk_fsm_exit(mdev);
> }
[Severity: High]
What happens here when the blocking removal event does not complete?
mtk_fsm_evt_submit() returns FSM_EVT_RET_FAIL if its kzalloc() fails, and
the result is discarded. In that case the FSM_STATE_OFF notifier chain
never runs, so ctrl_blk->ops->exit() = mtk_pcie_hif_exit() is never
called: the CLDMA IRQ callback stays registered, the CLDMA workqueue and
DMA pools stay alive, the TRB service kthreads keep running and
trans->available stays 1. mtk_trans_ctrl_exit() does not compensate:
drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.c:mtk_trans_ctrl_exit() {
mtk_ctrl_exit(mdev);
return 0;
}
and the devm-allocated trans/ctrl_blk/fsm are released once remove()
finishes, leaving those kthreads and the IRQ callback pointing at freed
memory. The wait can also expire (BLOCKING_EVT_TIMEOUT is 60s) without
dequeuing or cancelling the event:
ret = wait_event_timeout(fsm->evt_waitq,
(event->status != 0), BLOCKING_EVT_TIMEOUT);
so teardown then races with an FSM thread still using ctrl_blk/trans.
> static int mtk_pci_dev_start(struct mtk_md_dev *mdev)
> {
> + mtk_fsm_evt_submit(mdev, FSM_EVT_DEV_ADD, 0, NULL, 0, 0);
> + mtk_fsm_start(mdev);
> return 0;
> }
[Severity: Medium]
Should these two return values be propagated? mtk_pci_dev_start() is
declared int and mtk_pci_probe() already has an if (ret) handler for it,
but both calls are discarded and 0 is returned unconditionally.
mtk_fsm_evt_submit() returns FSM_EVT_RET_FAIL when its
kzalloc(sizeof(*event), ...) fails, so probe can succeed while the FSM
never leaves FSM_STATE_INVALID. Every later handshake event is then
rejected by mtk_fsm_startup_act():
if (fsm->state != FSM_STATE_ON && fsm->state != FSM_STATE_BOOTUP) {
ret = -EPROTO;
goto free_rt_data;
}
leaving the device unusable with only a generic error log.
> diff --git a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> index a3ff56ddf86fc..4b9c9db6ad715 100644
> --- a/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> +++ b/drivers/net/wwan/t9xx/pcie/mtk_trans_ctrl.h
> @@ -29,7 +29,6 @@
> enum mtk_hif_id {
> CLDMA0,
> CLDMA1,
> - CLDMA4,
> NR_CLDMA
> };
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports
2026-08-11 7:14 ` [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
@ 2026-08-17 22:36 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:36 UTC (permalink / raw)
To: jackbb_wu
Cc: Jakub Kicinski, loic.poulain, ryazanov.s.a, johannes,
andrew+netdev, davem, edumazet, pabeni, wen-zhi.huang,
shi-wei.yeh, Minano.tseng, matthias.bgg,
angelogioacchino.delregno, horms, corbet, skhan, robert_yu,
Jeff_Chang, linux-kernel, netdev, linux-arm-kernel,
linux-mediatek, linux-doc
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: wwan: t9xx: Add AT & MBIM WWAN ports
This adds AT and MBIM ports to the t9xx port infrastructure, creating them
through the WWAN framework when the modem FSM reaches the ready state. The
new wwan_port_ops implement start, stop, tx, tx_blocking and tx_poll, and
new CLDMA queues plus port configs are added for both channels.
A few questions on the new code below.
> diff --git a/drivers/net/wwan/t9xx/mtk_port.c b/drivers/net/wwan/t9xx/mtk_port.c
> index ee52b44ddf861..e67ac14c92861 100644
> --- a/drivers/net/wwan/t9xx/mtk_port.c
> +++ b/drivers/net/wwan/t9xx/mtk_port.c
> @@ -819,6 +819,29 @@ int mtk_port_ch_disable(struct mtk_port *port)
> return ret;
> }
>
> +static int mtk_port_enable_by_type(struct mtk_port_mngr *port_mngr, int tbl_type)
> +{
> + struct mtk_port **ports;
> + int ret, idx;
> +
> + if (tbl_type < 0 || tbl_type >= PORT_TBL_MAX)
> + return -EINVAL;
> +
> + ports = kcalloc(port_mngr->port_cnt, sizeof(struct mtk_port *), GFP_KERNEL);
> + if (!ports)
> + return -ENOMEM;
> +
> + ret = radix_tree_gang_lookup(&port_mngr->port_tbl[tbl_type],
> + (void **)ports, 0, port_mngr->port_cnt);
> + for (idx = 0; idx < ret; idx++) {
> + if (ports[idx]->enable)
> + ports_ops[ports[idx]->info.type]->enable(ports[idx]);
> + }
> +
> + kfree(ports);
> + return 0;
> +}
> +
> static void mtk_port_disable(struct mtk_port_mngr *port_mngr)
> {
> struct radix_tree_iter iter;
> @@ -850,6 +873,9 @@ void mtk_port_mngr_fsm_state_handler(struct mtk_fsm_param *fsm_param, void *arg)
> case FSM_STATE_OFF:
> mtk_port_disable(port_mngr);
> break;
> + case FSM_STATE_READY:
> + mtk_port_enable_by_type(port_mngr, PORT_TBL_MD);
> + break;
[Severity: Low]
Should the error from mtk_port_enable_by_type() be handled or at least
logged here?
If the kcalloc() in mtk_port_enable_by_type() fails, it returns -ENOMEM
before any port's enable() callback runs, so no AT or MBIM char device is
ever created and there is nothing in the log to explain it. The
surrounding code (mtk_port_ch_enable() and mtk_port_ch_disable()) does
emit a dev_err()/dev_warn() on failure.
> default:
> break;
> }
> diff --git a/drivers/net/wwan/t9xx/mtk_port_io.c b/drivers/net/wwan/t9xx/mtk_port_io.c
> index a4e264cd5f630..c249b973bd097 100644
> --- a/drivers/net/wwan/t9xx/mtk_port_io.c
> +++ b/drivers/net/wwan/t9xx/mtk_port_io.c
> @@ -41,6 +45,145 @@ static void mtk_port_struct_init(struct mtk_port *port)
[ ... ]
> +static int mtk_port_common_write(struct mtk_port *port, union user_buf buf, unsigned int len,
> + bool from_user_space)
> +{
> + u32 packet_size, left_cnt = len, cur_pos;
> + struct sk_buff *skb;
> + int ret;
> +
> + if (len == 0)
> + return -EINVAL;
> +
> +start_write:
> + ret = mtk_port_status_check(port);
> + if (ret)
> + goto end_write;
[ ... ]
> + ret = mtk_port_send_data(port, skb);
> + if (ret < 0) {
> + if (ret == -EINTR)
> + left_cnt -= packet_size;
> + goto end_write;
> + }
> +
> + left_cnt -= ret;
> + if (left_cnt)
> + goto start_write;
> + else
> + goto end_write;
> +
> +err_free_skb:
> + dev_kfree_skb_any(skb);
> +end_write:
> + return (len > left_cnt) ? (len - left_cnt) : ret;
> +}
[ ... ]
> @@ -233,6 +376,198 @@ static const struct port_ops port_internal_ops = {
[ ... ]
> +static int mtk_port_wwan_write(struct wwan_port *w_port, struct sk_buff *skb)
> +{
> + struct mtk_port *port = wwan_port_get_drvdata(w_port);
> + union user_buf user_buf;
> + int ret;
> +
> + if (unlikely(!skb->len)) {
> + consume_skb(skb);
> + return 0;
> + }
> +
> + port->info.flags &= ~PORT_F_BLOCKING;
> + user_buf.kbuf = (void *)skb->data;
> + ret = mtk_port_common_write(port, user_buf, skb->len, false);
> + if (ret < 0)
> + return ret;
> +
> + consume_skb(skb);
> + return 0;
> +}
[Severity: Medium]
Can a short write be silently reported to userspace as a complete write
here (and in mtk_port_wwan_write_blocking() below)?
mtk_port_common_write() returns a positive partial count whenever some
prefix of the request was sent before a failure:
end_write:
return (len > left_cnt) ? (len - left_cnt) : ret;
That happens when mtk_port_status_check() fails on a later loop iteration
(port disabled or flushed by a modem reset while a multi-chunk write is in
flight), and on the interrupted blocking path:
ret = mtk_port_send_data(port, skb);
if (ret < 0) {
if (ret == -EINTR)
left_cnt -= packet_size;
goto end_write;
}
Both tx callbacks only test ret < 0, then consume the skb and return 0.
The WWAN core treats zero as all-or-nothing in wwan_port_fops_write():
ret = wwan_port_op_tx(port, head, !!(filp->f_flags & O_NONBLOCK));
if (!ret)
return count;
With tx_mtu of 3456 bytes, any larger MBIM control message is split by the
goto start_write loop, so a truncated message would be reported as fully
written with no error. Should a positive return smaller than skb->len be
propagated as an error instead?
> +static int mtk_port_wwan_write_blocking(struct wwan_port *w_port, struct sk_buff *skb)
> +{
> + struct mtk_port *port = wwan_port_get_drvdata(w_port);
> + union user_buf user_buf;
> + int ret;
> +
> + if (unlikely(!skb->len)) {
> + consume_skb(skb);
> + return 0;
> + }
> +
> + port->info.flags |= PORT_F_BLOCKING;
> + user_buf.kbuf = (void *)skb->data;
> + ret = mtk_port_common_write(port, user_buf, skb->len, false);
> + if (ret < 0)
> + return ret;
> +
> + consume_skb(skb);
> + return 0;
> +}
[ ... ]
> +static void mtk_port_wwan_enable(struct mtk_port *port)
> +{
> + struct mtk_port_mngr *port_mngr;
> + int ret;
> +
> + port_mngr = port->port_mngr;
> +
> + if (test_bit(PORT_S_ENABLE, &port->status))
> + return;
> +
> + ret = mtk_port_ch_enable(port);
> + if (ret && ret != -EBUSY)
> + return;
[Severity: Medium]
Does this early return need a compensating mtk_port_ch_disable(), the way
the wwan_create_port() failure path below does?
The TRB_CMD_ENABLE is processed asynchronously by the trb kthread, and
mtk_port_ch_enable() only waits with a timeout:
ret = wait_event_timeout(port->trb_wq, trb->status <= 0,
MTK_DFLT_TRB_TIMEOUT);
if (!ret)
ret = -ETIMEDOUT;
By then mtk_ch_status_check() has already done:
trans->usr_cnt[que->hif_id][que->txqno]++;
if (trans->usr_cnt[que->hif_id][que->txqno] == 1)
break;
and at usr_cnt == 1 the trb thread runs mtk_cldma_open(), which allocates
the rings and starts CLDMA1 TXQ(5)/RXQ(5) for AT or TXQ(2)/RXQ(2) for
MBIM. On the -ETIMEDOUT path the queue stays started while the port stays
disabled, so RX data is dropped by mtk_port_wwan_recv() with -ENXIO.
usr_cnt is never reset, and mtk_port_wwan_disable() returns immediately
because PORT_S_ENABLE was never set, so on the next FSM_STATE_READY the
enable takes the usr_cnt != 1 branch, mtk_cldma_check_ch_cfg() finds the
queue not allocated and returns -EINVAL. Would that leave the AT/MBIM
channel permanently unusable for the lifetime of the device?
> + port->w_priv.w_port = wwan_create_port(port_mngr->ctrl_blk->mdev->dev,
> + port->w_priv.w_type,
> + &wwan_ops, NULL, port);
> + if (IS_ERR(port->w_priv.w_port)) {
> + dev_warn(port_mngr->ctrl_blk->mdev->dev,
> + "Failed to create wwan port for (%s)\n", port->info.name);
> + port->w_priv.w_port = NULL;
> + mtk_port_ch_disable(port);
> + return;
> + }
[Severity: Medium]
Can the RX path dereference the ERR_PTR that is stored into
port->w_priv.w_port before it is validated?
The store happens without w_lock and the error value is only replaced with
NULL after the IS_ERR() test. The consumer takes w_lock but only checks
for NULL:
mutex_lock(&port->w_priv.w_lock);
if (!port->w_priv.w_port) {
mutex_unlock(&port->w_priv.w_lock);
return -ENXIO;
}
wwan_port_rx(port->w_priv.w_port, skb);
RX is already live at this point: the earlier mtk_port_ch_enable()
installs trb_open_priv->rx_done = mtk_port_rx_dispatch and starts the
CLDMA RX queue, and dispatch runs from the CLDMA workqueue
mtk_cldma_rx_done_work() -> rxq->rx_done(), i.e. a different thread than
the FSM thread running enable. If wwan_create_port() fails, a packet
arriving in that window would reach wwan_port_rx() with an error pointer.
mtk_port_wwan_disable() takes w_lock for its store to the same field, so
would using a local variable and only publishing the validated pointer
under w_lock be more consistent here?
[Severity: Low]
This isn't a bug, but would passing a wwan_port_caps here be preferable to
NULL? With caps == NULL, wwan_create_port() sets:
port->frag_len = caps ? caps->frag_len : SIZE_MAX;
port->headroom_len = caps ? caps->headroom_len : 0;
so wwan_port_fops_write() does one GFP_KERNEL allocation sized to the
whole user-supplied write count:
frag_len = min(remain, port->frag_len);
skb = alloc_skb(frag_len + port->headroom_len, GFP_KERNEL);
and the driver then copies the data again into tx_mtu-sized skbs. Setting
frag_len to the tx MTU and headroom_len to sizeof(struct mtk_ccci_header),
as t7xx_port_wwan.c does, would bound that allocation and drop the extra
copy.
> + set_bit(PORT_S_WR, &port->status);
> + set_bit(PORT_S_ENABLE, &port->status);
> +}
[Severity: Low]
Should these bits be set before wwan_create_port() rather than after?
wwan_create_port() -> wwan_port_register_wwan() -> device_add() publishes
/dev/wwanXat0 or /dev/wwanXmbim0 and fires the udev event, so a listener
such as ModemManager can open() the node before these set_bit() calls run.
In that window mtk_port_wwan_open() -> mtk_port_common_open() fails with
-ENODEV because PORT_S_ENABLE is not set yet, and a write landing between
the two set_bit() calls would fail with -EBADF from
mtk_port_status_check().
[ ... ]
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry
2026-08-11 7:14 ` [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry Jack Wu via B4 Relay
@ 2026-08-17 22:38 ` Jakub Kicinski
0 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:38 UTC (permalink / raw)
To: Jack Wu via B4 Relay
Cc: jackbb_wu, Loic Poulain, Sergey Ryazanov, Johannes Berg,
Andrew Lunn, David S. Miller, Eric Dumazet, Paolo Abeni,
Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, Jonathan Corbet,
Shuah Khan, Robert Yu, Jeff Chang, linux-kernel, netdev,
linux-arm-kernel, linux-mediatek, linux-doc
On Tue, 11 Aug 2026 15:14:29 +0800 Jack Wu via B4 Relay wrote:
> Add MAINTAINERS entry for the MediaTek T9XX 5G WWAN modem device
> driver.
Please squash this into the first patch
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
` (6 preceding siblings ...)
2026-08-11 7:14 ` [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry Jack Wu via B4 Relay
@ 2026-08-17 22:39 ` Jakub Kicinski
7 siblings, 0 replies; 15+ messages in thread
From: Jakub Kicinski @ 2026-08-17 22:39 UTC (permalink / raw)
To: Jack Wu via B4 Relay
Cc: jackbb_wu, Loic Poulain, Sergey Ryazanov, Johannes Berg,
Andrew Lunn, David S. Miller, Eric Dumazet, Paolo Abeni,
Wen-Zhi Huang, Shi-Wei Yeh, Minano Tseng, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, Jonathan Corbet,
Shuah Khan, Robert Yu, Jeff Chang, linux-kernel, netdev,
linux-arm-kernel, linux-mediatek, linux-doc
On Tue, 11 Aug 2026 15:14:22 +0800 Jack Wu via B4 Relay wrote:
> * Core logic - Contains the core logic to keep the
> device working. It includes:
>
> * FSM (Finite State Machine) - Monitors the state
> of the device, and notifies each module when the
> state changes.
This FSM exists in the driver, even tho you call it "core logic".
Please provide more details in the patch description of patch 5
and consider whether any of this logic should live in the WWAN
framework.
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-08-17 22:40 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-11 7:14 [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 1/7] net: wwan: t9xx: Add PCIe core Jack Wu via B4 Relay
2026-08-17 22:35 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 2/7] net: wwan: t9xx: Add control plane transaction layer Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 3/7] net: wwan: t9xx: Add control DMA interface Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 4/7] net: wwan: t9xx: Add control port Jack Wu via B4 Relay
2026-08-11 7:14 ` [PATCH v6 5/7] net: wwan: t9xx: Add FSM thread Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 6/7] net: wwan: t9xx: Add AT & MBIM WWAN ports Jack Wu via B4 Relay
2026-08-17 22:36 ` Jakub Kicinski
2026-08-11 7:14 ` [PATCH v6 7/7] net: wwan: t9xx: Add maintainers entry Jack Wu via B4 Relay
2026-08-17 22:38 ` Jakub Kicinski
2026-08-17 22:39 ` [PATCH v6 0/7] net: wwan: t9xx: Add MediaTek T9XX WWAN driver Jakub Kicinski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox