[PATCH v1] wifi: mt76: mt7928: Add debugfs for stats to provide debug purpose

Eason Lai <[email protected]>
Newsgroups org.kernel.vger.linux-wireless,org.infradead.lists.linux-mediatek
Message-ID <[email protected]>
From: Michael Shih <[email protected]>

Usage:
(1) Connect STA to AP  (You can use dhclient CMD to ask for DHCP IP, to use ping to AP)
    DHCP IP CMD:  dhclient ${WLAN_IF} -v

   Example: dhclient wlan0 -v

   ## (To simulate traffic) Ping 10 packets for every second to AP
   ping 192.168.0.XXX  -i0.1

(2) (Get stats) Use command, phyX is your WiFi interface:
     cat /sys/kernel/debug/ieee80211/phyX/mt76/stats

(3) You will see below stats info:
(STA) connected AP MAC Address = a8:42:a1:46:c4:6b
                        BssIdx = [0]
                        StaRecIdx = [1],
                        RSSI = -25
                        Link_Speed = 2402000 (kbit/s)
                        temperature = 40
                        Tx success = 716
                        Tx fail to Rcv ACK after retry = 0
                        Rx Mpdu = 23
                        Rx Fcs Error = 1 (MPDU)
                        Rx FIFO full = 0 (MPDU)
                        TRX_AGG_RANGE[0] = 0 (PPDU)
                        TRX_AGG_RANGE[1] = 0 (PPDU)
                        TRX_AGG_RANGE[2] = 0 (PPDU)
                        TRX_AGG_RANGE[3] = 0 (PPDU)
                        TRX_AGG_RANGE[4] = 0 (PPDU)
                        TRX_AGG_RANGE[5] = 0 (PPDU)
                        TRX_AGG_RANGE[6] = 0 (PPDU)
                        TRX_AGG_RANGE[7] = 0 (PPDU)
                        MPDUs in AMPDUs transmitted = 0 (MPDU)
                        MPDUs in AMPDUs transmitted with ACK reply = 0 (MPDU)
                        rate1_tx_cnt = 714
                        rate1_fail_cnt = 223
                        train_up = 2310
                        train_down = 3080
                        is_force_tx_stream = 0
                        is_force_se_off = 0
                        max_ampdu_factor = 9
                        tx_rate_up_penalty = 0
                        low_traffic_mode = 2
                        low_traffic_count = 0
                        low_traffic_dashboard = 3
                        dynamic_sgi_state = 0
                        dynamic_sgi_score = 2
                        dynamic_bw_state = 0
                        dynamic_gband_256qam_state = 3
                        vht_non_sp_rate_state = 0
                        Last TX Rate = 24M, BW20, OFDM, BCC, SPE24
                        Chip Out TX Power = +16.5 dBm
                        Cur Rate Index = 0
Rate index[0]    --> NSS2_MCS11, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[1]        NSS2_MCS9, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[2]        NSS2_MCS7, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[3]        NSS2_MCS4, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[4]        NSS2_MCS3, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[5]        NSS1_MCS2, BW160/BW8080, SGI, HE_SU, LDPC
Rate index[6]        NSS1_MCS1, BW80, SGI, HE_SU, LDPC
Rate index[7]        NSS1_MCS0, BW80, SGI, HE_SU, LDPC

mib state:
=== Rx Related Counters ===
                        Rx with CRC = 24020 (MPDU)
                        Rx drop due to out of resource = 0 (MPDU)
                        Rx Mpdu = 93223 (MPDU)
                        Rx AMpdu = 668 (PPDU)
                        Rx PF Drop = 0 (MPDU)
                        Rx Len Mismatch = 5853 (PPDU)

=== Phy/Timing Related Counters ===
                        ChannelIdleCnt = 1040434
                        CCA_NAV_Tx_Time = 18510036
                        Rx_MDRDY_CNT = 166580 (PPDU)
                        CCK_MDRDY = 7782
                        OFDM_MDRDY = 11912731
                        OFDM_GREEN_MDRDY = 0
                        Prim CCA Time = 17917650
                        Sec CCA Time = 14922034
                        Prim ED Time = 5677528

=== Tx Related Counters(Generic) ===
                        BeaconTxCnt = 0
                        Tx 40MHz Cnt = 279  (MPDU)
                        Tx 80MHz Cnt = 44  (MPDU)
                        Tx 160MHz Cnt = 712  (MPDU)

=== BSSID[0] Related Counters ===
                        BA Miss Cnt = 210  (PPDU)
                        RTS Tx Cnt = 1003  (MPDU)
                        Frame Retry Cnt = 239 (MPDU)
                        Frame Retry 2 Cnt = 71 (MPDU)
                        RTS Retry Cnt = 152 (MPDU)
                        Ack Failed Cnt = 319 (PPDU)

=== AMPDU Related Counters ===
                        Tx AMPDU_Pkt_Cnt = 705  (PPDU)
                        Tx AMPDU_MPDU_Pkt_Cnt = 713  (MPDU)
                        AMPDU Tx success = 503 (MPDU)
                        AMPDU Tx fail count   = 210  (MPDU), PER=29.4%

Signed-off-by: Michael Shih <[email protected]>
---
 .../wireless/mediatek/mt76/mt76_connac_mcu.h  |  177 +++
 .../wireless/mediatek/mt76/mt7925/Makefile    |    2 +-
 .../wireless/mediatek/mt76/mt7925/debugfs.c   | 1054 +++++++++++++++++
 .../net/wireless/mediatek/mt76/mt7925/stats.c |  585 +++++++++
 .../net/wireless/mediatek/mt76/mt7925/stats.h |  305 +++++
 5 files changed, 2122 insertions(+), 1 deletion(-)
 create mode 100644 drivers/net/wireless/mediatek/mt76/mt7925/stats.c
 create mode 100644 drivers/net/wireless/mediatek/mt76/mt7925/stats.h

diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
index 0ededa569e9a..72cc69750f47 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
@@ -1483,6 +1483,183 @@ enum {
 	MT_NIC_CAP_EML_CAP = 0x22,
 };
 
+enum {
+	UNI_CMD_ID_STATISTICS_BASIC = 0,
+	UNI_CMD_ID_STATISTICS_LINK_QUALITY = 1,
+	UNI_CMD_ID_STATISTICS_STA = 2,
+	UNI_CMD_ID_STATISTICS_BUG_REPORT = 3,
+	UNI_CMD_ID_STATISTICS_TX_STATS = 4,
+	UNI_CMD_ID_STATISTICS_ALL_STATS = 5,
+	UNI_CMD_ID_STATISTICS_EML_STATS = 6,
+	UNI_CMD_ID_STATISTICS_REGULAR_STATS = 7,
+	UNI_CMD_ID_STATISTICS_BSS_LINK_QUALITY = 8,
+	UNI_CMD_ID_STATISTICS_ML_CHNL_COND = 9,
+	UNI_CMD_ID_STATISTICS_WTBL = 10,
+	UNI_CMD_ID_STATISTICS_LINK_LAYER_STATS = 128,
+	UNI_CMD_ID_STATISTICS_PPDU_LATENCY = 129,
+	UNI_CMD_ID_STATISTICS_CURRENT_TX_RATE = 130,
+	UNI_CMD_ID_STATISTICS_SET_TX_BITRATE_MONITOR = 131,
+	UNI_CMD_ID_STATISTICS_GET_PREDICT_TX_BITRATE = 132,
+	UNI_CMD_ID_STATISTICS_SET_ATPS_TYPE_RATE_MODE = 133,
+	UNI_CMD_ID_STATISTICS_SET_BEACON_REPORT = 134,
+	UNI_CMD_ID_STATISTICS_BSS_CURRENT_TX_RATE = 135,
+	UNI_CMD_ID_STATISTICS_GET_BSS_PREDICT_TX_BITRATE = 136,
+	UNI_CMD_ID_STATISTICS_MAX_NUM
+};
+
+enum {
+	UNI_CMD_MIB_DATA,
+};
+
+enum mt7925_mib_counter {
+	UNI_CMD_MIB_CNT_RX_FCS_ERR = 0,
+	UNI_CMD_MIB_CNT_RX_FIFO_OVERFLOW = 1,
+	UNI_CMD_MIB_CNT_RX_MPDU = 2,
+	UNI_CMD_MIB_CNT_AMPDU_RX_COUNT = 3,
+	UNI_CMD_MIB_CNT_RX_TOTAL_BYTE = 4,
+	UNI_CMD_MIB_CNT_RX_VALID_AMPDU_SF = 5,
+	UNI_CMD_MIB_CNT_RX_VALID_BYTE = 6,
+	UNI_CMD_MIB_CNT_CHANNEL_IDLE = 7,
+	UNI_CMD_MIB_CNT_VEC_DROP = 8,
+	UNI_CMD_MIB_CNT_DELIMITER_FAIL = 9,
+	UNI_CMD_MIB_CNT_VEC_MISMATCH = 10,
+	UNI_CMD_MIB_CNT_MDRDY = 11,
+	UNI_CMD_MIB_CNT_RX_CCK_MDRDY_TIME = 12,
+	UNI_CMD_MIB_CNT_RX_OFDM_LG_MIXED_MDRDY_TIME = 13,
+	UNI_CMD_MIB_CNT_RX_OFDM_GREEN_MDRDY_TIME = 14,
+	UNI_CMD_MIB_CNT_PF_DROP = 15,
+	UNI_CMD_MIB_CNT_LEN_MISMATCH = 16,
+	UNI_CMD_MIB_CNT_P_CCA_TIME = 17,
+	UNI_CMD_MIB_CNT_S_CCA_TIME = 18,
+	UNI_CMD_MIB_CNT_CCA_NAV_TX_TIME = 19,
+	UNI_CMD_MIB_CNT_P_ED_TIME = 20,
+	UNI_CMD_MIB_CNT_BCN_TX = 21,
+	UNI_CMD_MIB_CNT_TX_BW_20MHZ = 22,
+	UNI_CMD_MIB_CNT_TX_BW_40MHZ = 23,
+	UNI_CMD_MIB_CNT_TX_BW_80MHZ = 24,
+	UNI_CMD_MIB_CNT_TX_BW_160MHZ = 25,
+	UNI_CMD_RMAC_CNT_OBSS_AIRTIME = 26,
+	UNI_CMD_RMAC_CNT_NONWIFI_AIRTIME = 27,
+	UNI_CMD_MIB_CNT_TX_DUR_CNT = 28,
+	UNI_CMD_MIB_CNT_RX_DUR_CNT = 29,
+	UNI_CMD_MIB_CNT_BA_CNT = 30,
+	UNI_CMD_MIB_CNT_MAC2PHY_TX_TIME = 31,
+	UNI_CMD_MIB_CNT_RX_OUT_OF_RANGE_COUNT = 32,
+	UNI_CMD_MIB_CNT_IBF_TX = 33,
+	UNI_CMD_MIB_CNT_EBF_TX = 34,
+	UNI_CMD_MIB_CNT_MUBF_TX = 35,
+	UNI_CMD_MIB_CNT_RX_BF_VHTFBK = 36,
+	UNI_CMD_MIB_CNT_RX_BF_HTFBK = 37,
+	UNI_CMD_MIB_CNT_RX_BF_HEFBK = 38,
+	UNI_CMD_MIB_CNT_BFEE_TXFBK_BFPOLL_TRI = 39,
+	UNI_CMD_MIB_CNT_BFEE_TXFBK_TRI = 40,
+	UNI_CMD_MIB_CNT_BFEE_RX_FBKCQI = 41,
+	UNI_CMD_MIB_CNT_BFEE_RX_NDP = 42,
+	UNI_CMD_MIB_CNT_RX_BF_IBF_UPT = 43,
+	UNI_CMD_MIB_CNT_RX_BF_EBF_UPT = 44,
+	UNI_CMD_MIB_CNT_BFEE_SP_ABORT = 45,
+	UNI_CMD_MIB_CNT_BFEE_TB_LEN_ERR = 46,
+	UNI_CMD_MIB_CNT_BFEE_TXFBK_MUTE = 47,
+	UNI_CMD_MIB_CNT_BFEE_TMAC_ABORT = 48,
+	UNI_CMD_MIB_CNT_BFEE_TXFBK_CPL = 49,
+	UNI_CMD_MIB_CNT_BFEE_COANT_BLKTX = 50,
+	UNI_CMD_MIB_CNT_BFEE_FBK_SEG = 51,
+	UNI_CMD_MIB_CNT_NAV_TIME = 52,
+	UNI_CMD_MIB_CNT_TX_ABORT_CNT1 = 53,
+	UNI_CMD_MIB_CNT_TX_ABORT_CNT0 = 54,
+	UNI_CMD_MIB_CNT_TX_ABORT_CNT3 = 55,
+	UNI_CMD_MIB_CNT_TX_ABORT_CNT2 = 56,
+	UNI_CMD_MIB_CNT_BSS0_RTS_TX_CNT = 57,
+	UNI_CMD_MIB_CNT_BSS1_RTS_TX_CNT = 58,
+	UNI_CMD_MIB_CNT_BSS2_RTS_TX_CNT = 59,
+	UNI_CMD_MIB_CNT_BSS3_RTS_TX_CNT = 60,
+	UNI_CMD_MIB_CNT_BSS0_RTS_RETRY = 61,
+	UNI_CMD_MIB_CNT_BSS1_RTS_RETRY = 62,
+	UNI_CMD_MIB_CNT_BSS2_RTS_RETRY = 63,
+	UNI_CMD_MIB_CNT_BSS3_RTS_RETRY = 64,
+	UNI_CMD_MIB_CNT_BSS0_BA_MISS = 65,
+	UNI_CMD_MIB_CNT_BSS1_BA_MISS = 66,
+	UNI_CMD_MIB_CNT_BSS2_BA_MISS = 67,
+	UNI_CMD_MIB_CNT_BSS3_BA_MISS = 68,
+	UNI_CMD_MIB_CNT_BSS0_ACK_FAIL = 69,
+	UNI_CMD_MIB_CNT_BSS1_ACK_FAIL = 70,
+	UNI_CMD_MIB_CNT_BSS2_ACK_FAIL = 71,
+	UNI_CMD_MIB_CNT_BSS3_ACK_FAIL = 72,
+	UNI_CMD_MIB_CNT_BSS0_FRAME_RETRY = 73,
+	UNI_CMD_MIB_CNT_BSS1_FRAME_RETRY = 74,
+	UNI_CMD_MIB_CNT_BSS2_FRAME_RETRY = 75,
+	UNI_CMD_MIB_CNT_BSS3_FRAME_RETRY = 76,
+	UNI_CMD_MIB_CNT_BSS0_FRAME_RETRY_2 = 77,
+	UNI_CMD_MIB_CNT_BSS1_FRAME_RETRY_2 = 78,
+	UNI_CMD_MIB_CNT_BSS2_FRAME_RETRY_2 = 79,
+	UNI_CMD_MIB_CNT_BSS3_FRAME_RETRY_2 = 80,
+	UNI_CMD_MIB_CNT_RX_A1_SEARCH = 81,
+	UNI_CMD_MIB_CNT_RX_DROP_MPDU = 82,
+	UNI_CMD_MIB_CNT_RX_UNWANTED = 83,
+	UNI_CMD_MIB_CNT_RX_FCS_OK = 84,
+	UNI_CMD_MIB_CNT_SU_TX_OK = 85,
+	UNI_CMD_MIB_CNT_TX_FULL_BW = 86,
+	UNI_CMD_MIB_CNT_TX_AUTO_BW = 87,
+	UNI_CMD_MIB_CNT_CCA_SAMPLE_IDLE = 88,
+	UNI_CMD_MIB_CNT_CCA_SAMPLE_ACTIVE = 89,
+	UNI_CMD_MIB_CNT_CCA_TIME = 90,
+	UNI_CMD_MIB_CNT_S_20BW_CCA_TIME = 91,
+	UNI_CMD_MIB_CNT_S_40BW_CCA_TIME = 92,
+	UNI_CMD_MIB_CNT_S_80BW_CCA_TIME = 93,
+	UNI_CMD_MIB_CNT_S_160BW_CCA_TIME = 94,
+	UNI_CMD_MIB_CNT_S_P20BW_0_ED_TIME = 95,
+	UNI_CMD_MIB_CNT_S_P20BW_1_ED_TIME = 96,
+	UNI_CMD_MIB_CNT_S_P20BW_2_ED_TIME = 97,
+	UNI_CMD_MIB_CNT_S_P20BW_3_ED_TIME = 98,
+	UNI_CMD_MIB_CNT_S_P20BW_4_ED_TIME = 99,
+	UNI_CMD_MIB_CNT_S_P20BW_5_ED_TIME = 100,
+	UNI_CMD_MIB_CNT_S_P20BW_6_ED_TIME = 101,
+	UNI_CMD_MIB_CNT_S_P20BW_7_ED_TIME = 102,
+	UNI_CMD_MIB_CNT_S_P20BW_8_ED_TIME = 103,
+	UNI_CMD_MIB_CNT_S_P20BW_9_ED_TIME = 104,
+	UNI_CMD_MIB_CNT_S_P20BW_10_ED_TIME = 105,
+	UNI_CMD_MIB_CNT_S_P20BW_11_ED_TIME = 106,
+	UNI_CMD_MIB_CNT_S_P20BW_12_ED_TIME = 107,
+	UNI_CMD_MIB_CNT_S_P20BW_13_ED_TIME = 108,
+	UNI_CMD_MIB_CNT_S_P20BW_14_ED_TIME = 109,
+	UNI_CMD_MIB_CNT_S_P20BW_15_ED_TIME = 110,
+	UNI_CMD_MIB_CNT_TX_BW_320MHZ = 111,
+	UNI_CMD_MIB_CNT_TX_DDLMT_RNG0 = 112, /* Rng0~Rng4 112~116 */
+	UNI_CMD_MIB_CNT_BSS0_FRAME_RETRY_3 = 117, /* 117~120 */
+	UNI_CMD_MIB_CNT_BSS0_TX = 121, /* 121~124 */
+	UNI_CMD_MIB_CNT_BSS0_TX_DATA = 125, /* 125~128 */
+	UNI_CMD_MIB_CNT_BSS0_TX_BYTE = 129, /* 129~132 */
+	UNI_CMD_MIB_CNT_RX_OK_BSS0 = 133, /* 133~136 */
+	UNI_CMD_MIB_CNT_RX_BYTE_BSS0 = 137, /* 137~140 */
+	UNI_CMD_MIB_CNT_RX_DATA_BSS0 = 141, /* 141~144 */
+	UNI_CMD_MIB_CNT_MBSS0_TX_OK = 145, /* 145~160 */
+	UNI_CMD_MIB_CNT_MBSS0_TX_BYTE = 161, /* 161~176 */
+	UNI_CMD_MIB_CNT_RX_OK_MBSS0 = 177, /* 177~192 */
+	UNI_CMD_MIB_CNT_RX_BYTE_MBSS0 = 193, /* 193~208 */
+	UNI_CMD_MIB_CNT_AMPDU = 209,
+	UNI_CMD_MIB_CNT_AMPDU_MPDU = 210,
+	UNI_CMD_MIB_CNT_AMPDU_ACKED = 211,
+	UNI_CMD_MIB_CNT_MAX_NUM
+};
+
+enum ENUM_UNI_CMD_GET_STATISTICS_TAG {
+	UNI_CMD_GET_STATISTICS_TAG_BASIC = 0,
+	UNI_CMD_GET_STATISTICS_TAG_LINK_QUALITY = 1,
+	UNI_CMD_GET_STATISTICS_TAG_STA = 2,
+	UNI_CMD_GET_STATISTICS_TAG_BUG_REPORT = 3,
+	UNI_CMD_GET_STATISTICS_TAG_EML_STATS = 6,
+	UNI_CMD_GET_STATISTICS_TAG_REGULAR_STATS = 7,
+	UNI_CMD_GET_STATISTICS_TAG_BSS_LINK_QUALITY = 8,
+	UNI_CMD_GET_STATISTICS_TAG_GET_ML_CHNL_COND = 9,
+	UNI_CMD_GET_STATISTICS_TAG_STAT_WTBL = 10,
+	UNI_CMD_GET_STATISTICS_TAG_LINK_LAYER_STATS = 0x80,
+	UNI_CMD_GET_STATISTICS_TAG_PPDU_LATENCY,
+	UNI_CMD_GET_STATISTICS_TAG_CURRENT_TX_RATE,
+	UNI_CMD_GET_STATISTICS_TAG_BEACON_REPORT = 0x86,
+	UNI_CMD_GET_STATISTICS_TAG_BSS_CURRENT_TX_RATE = 0x87,
+	UNI_CMD_GET_STATISTICS_TAG_BSS_PRED_TX_BITRATE = 0x88,
+};
+
 #define UNI_WOW_DETECT_TYPE_MAGIC		BIT(0)
 #define UNI_WOW_DETECT_TYPE_ANY			BIT(1)
 #define UNI_WOW_DETECT_TYPE_DISCONNECT		BIT(2)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/Makefile b/drivers/net/wireless/mediatek/mt76/mt7925/Makefile
index f9dcc0bba393..3863a89b8974 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/Makefile
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/Makefile
@@ -4,7 +4,7 @@ obj-$(CONFIG_MT7925_COMMON) += mt7925-common.o
 obj-$(CONFIG_MT7925E) += mt7925e.o
 obj-$(CONFIG_MT7925U) += mt7925u.o
 
-mt7925-common-y := mac.o mcu.o regd.o main.o init.o debugfs.o nan.o
+mt7925-common-y := mac.o mcu.o regd.o main.o init.o debugfs.o nan.o stats.o
 mt7925-common-$(CONFIG_NL80211_TESTMODE) += testmode.o
 mt7925e-y := pci.o pci_mac.o pci_mcu.o
 mt7925u-y := usb.o
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c b/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
index d01ff49de47a..e3d2d9ef20fe 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/debugfs.c
@@ -3,6 +3,7 @@
 
 #include "mt7925.h"
 #include "mcu.h"
+#include "stats.h"
 
 static int
 mt7925_reg_set(void *data, u64 val)
@@ -286,6 +287,1057 @@ static int mt7925_chip_reset(void *data, u64 val)
 
 DEFINE_DEBUGFS_ATTRIBUTE(fops_reset, NULL, mt7925_chip_reset, "%lld\n");
 
+/* fixedrate input format (12 hex digits):
+ * 0x[WCID(2)][Mode][BW][MCS][Nss][SGI][Preamble][STBC][LDPC][SPE_EN][HeLtf]
+ * The RA tag is fixed to SET_FIXED_RATE internally.
+ * WCID is 2 nibbles, all others are 1 nibble.
+ *
+ * [WCID]    Wireless Client ID
+ * [Mode]    CCK=0, OFDM=1, HT=2, GF=3, VHT=4, PLR=5, HE_SU=8, HE_ER_SU=9,
+ *           HE_TRIG=10, HE_MU=11, EHT_ER=13, EHT_TB=14, EHT_SU and EHT_MU=15
+ * [BW]      BW20=0, BW40=1, BW80=2, BW160=3, BW320=4
+ * [MCS]     CCK=0~3, OFDM=0~7, HT=0~32, VHT=0~9, HE=0~11, EHT=0~13, EHT_ER=14~15
+ * [Nss]     1~8
+ * [GI]      HT/VHT: 0=Long, 1=Short; HE/EHT: 0=0.8us, 1=1.6us, 2=3.2us
+ * [Preamble] Long=0, Short=other
+ * [STBC]    Enable=1, Disable=0
+ * [LDPC]    BCC=0, LDPC=1 (driver sends the FW's "on" value 7)
+ * [SPE_EN]  spatial extension index
+ * [HeLtf]   1X=0, 2X=1, 4X=2 (NOTE: EHT modes require HeLtf=1)
+ */
+static int
+mt7925_fixedrate_set(void *data, u64 val)
+{
+	struct mt792x_dev *dev = data;
+	struct mt7925_ra_fixed_rate_v1 rate = {};
+
+	dev->fixed_rate = val;
+
+	rate.wlan_idx       = cpu_to_le16((val >> 40) & 0xff);
+	rate.phy_mode       = (val >> 36) & 0xf;
+	rate.bw             = (val >> 32) & 0xf;
+	rate.mcs            = (val >> 28) & 0xf;
+	rate.nss            = (val >> 24) & 0xf;
+	rate.short_gi       = cpu_to_le16((val >> 20) & 0xf);
+	rate.short_preamble = (val >> 16) & 0xf;
+	rate.stbc           = (val >> 12) & 0xf;
+	rate.ecc            = ((val >> 8) & 0xf) ? 7 : 0;
+	rate.spe            = (val >> 4) & 0xf;
+	rate.he_ltf         = cpu_to_le16((val >> 0) & 0xf);
+
+	mt792x_mutex_acquire(dev);
+	mt7925_mcu_set_fixed_rate(dev, &rate);
+	mt792x_mutex_release(dev);
+
+	return 0;
+}
+
+static int
+mt7925_fixedrate_get(void *data, u64 *val)
+{
+	struct mt792x_dev *dev = data;
+
+	*val = dev->fixed_rate;
+
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_fixedrate, mt7925_fixedrate_get,
+			 mt7925_fixedrate_set, "0x%llx\n");
+
+/* autorate input format (6 hex digits):
+ * 0x[Tag(2)][WCID(2)][En][Mode]
+ * The RA tag is fixed to SET_AUTO_RATE internally.
+ * WCID is 2 nibbles, En and Mode are 1 nibble each.
+ *
+ * [WCID] Wireless Client ID
+ * [En]   Auto rate: Enable=1, Disable=0
+ * [Mode] don't care (RA picks the rate)
+ */
+static int
+mt7925_autorate_set(void *data, u64 val)
+{
+	struct mt792x_dev *dev = data;
+	u16 wlan_idx = (val >> 8) & 0xff;
+	bool enable = (val >> 4) & 0xf;
+	u8 mode = val & 0xf;
+
+	dev->auto_rate = val;
+
+	mt792x_mutex_acquire(dev);
+	mt7925_mcu_set_auto_rate(dev, wlan_idx, enable, mode);
+	mt792x_mutex_release(dev);
+
+	return 0;
+}
+
+static int
+mt7925_autorate_get(void *data, u64 *val)
+{
+	struct mt792x_dev *dev = data;
+
+	*val = dev->auto_rate;
+
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_autorate, mt7925_autorate_get,
+			 mt7925_autorate_set, "0x%llx\n");
+
+/* WTBL dump
+ *
+ * Usage:
+ *   echo <idx> > /sys/kernel/debug/ieee80211/phy1/mt76/wtbl_idx
+ *   cat  /sys/kernel/debug/ieee80211/phy1/mt76/get_wtbl
+ *
+ * LMAC WTBL has 36 dwords per entry (connac3x).
+ */
+#define MT792x_WTBL_LMAC_DWORDS	36
+
+static int
+mt7925_wtbl_idx_set(void *data, u64 val)
+{
+	struct mt792x_dev *dev = data;
+
+	dev->wtbl_dump_idx = (u32)val;
+
+	return 0;
+}
+
+static int
+mt7925_wtbl_idx_get(void *data, u64 *val)
+{
+	struct mt792x_dev *dev = data;
+
+	*val = dev->wtbl_dump_idx;
+
+	return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_wtbl_idx, mt7925_wtbl_idx_get,
+			 mt7925_wtbl_idx_set, "%llu\n");
+
+static int
+mt7925_wtbl_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u32 idx = dev->wtbl_dump_idx;
+	u32 wdu_cr, wducr_val, base;
+	u32 dw0, dw1;
+	int i;
+
+	if (!mt76_is_mmio(&dev->mt76)) {
+		seq_puts(s, "WTBL direct dump not supported on this interface\n");
+		return 0;
+	}
+
+	if (idx >= MT792x_WTBL_SIZE) {
+		seq_printf(s, "Invalid WTBL idx %u (max %u)\n",
+			   idx, MT792x_WTBL_SIZE - 1);
+		return 0;
+	}
+
+	mt792x_mutex_acquire(dev);
+
+	/* mt7925_mac_wtbl_lmac_addr() writes WDUCR group selection and
+	 * returns the mapped register address for DW 0 of this WCID.
+	 */
+	base = mt7925_mac_wtbl_lmac_addr(dev, idx, 0);
+	wdu_cr = is_mt7928(&dev->mt76) ? MT7928_WTBLON_TOP_WDUCR :
+					  MT7925_WTBLON_TOP_WDUCR;
+	wducr_val = mt76_rr(dev, wdu_cr);
+
+	/* DW0[15:0] + DW1[31:0] encode the link address */
+	dw0 = mt76_rr(dev, base);
+	dw1 = mt76_rr(dev, base + 4);
+
+	seq_printf(s, "Dump WTBL info of WLAN_IDX: %u\n", idx);
+	seq_printf(s, "LMAC WTBL Addr: group:0x%08x=0x%08x addr: 0x%08x\n",
+		   wdu_cr, wducr_val, base);
+	seq_printf(s, "LinkAddr: %02x:%02x:%02x:%02x:%02x:%02x\n",
+		   dw1 & 0xff, (dw1 >> 8) & 0xff,
+		   (dw1 >> 16) & 0xff, (dw1 >> 24) & 0xff,
+		   dw0 & 0xff, (dw0 >> 8) & 0xff);
+
+	seq_puts(s, "\nLMAC WTBL raw data:\n");
+	for (i = 0; i < MT792x_WTBL_LMAC_DWORDS; i++)
+		seq_printf(s, "DW%02d: 0x%08x\n", i,
+			   mt76_rr(dev, base + i * 4));
+
+	mt792x_mutex_release(dev);
+
+	return 0;
+}
+
+static int
+mt7925_vefuse_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u16 size = dev->vefuse_cap.size;
+	int off = 0;
+	u8 *buf;
+	int ret = 0;
+
+	if (!dev->vefuse_cap.present || !size) {
+		seq_puts(s, "vefuse: no region in FW image\n");
+		return 0;
+	}
+
+	buf = kzalloc(size, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	mt792x_mutex_acquire(dev);
+	while (off < (int)size) {
+		u16 chunk = min_t(u16, 512, size - off);
+
+		ret = mt7925_mcu_read_vefuse(dev, off, chunk, buf + off);
+		if (ret)
+			break;
+		off += chunk;
+	}
+	mt792x_mutex_release(dev);
+
+	if (ret) {
+		seq_printf(s, "vefuse: read failed at 0x%04x (%d)\n", off, ret);
+		kfree(buf);
+		return 0;
+	}
+
+	seq_printf(s, "vefuse content: %u bytes\n", size);
+	for (off = 0; off < (int)size; off += 16)
+		seq_printf(s, "%04x: %*ph\n", off,
+			   (int)min_t(u16, 16, size - off), buf + off);
+
+	kfree(buf);
+
+	return 0;
+}
+
+/* send one perf_ind carrying the written value in every tx/rx byte field */
+static int mt7925_perf_ind_set(void *data, u64 val)
+{
+	struct mt792x_dev *dev = data;
+
+	return mt792x_perf_ind_trigger(dev, val);
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_perf_ind, NULL, mt7925_perf_ind_set, "%lld\n");
+
+static int mt7925_perf_ind_enable_get(void *data, u64 *val)
+{
+	struct mt792x_dev *dev = data;
+
+	*val = dev->perf.enabled;
+
+	return 0;
+}
+
+static int mt7925_perf_ind_enable_set(void *data, u64 val)
+{
+	struct mt792x_dev *dev = data;
+
+	dev->perf.enabled = !!val;
+
+	return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_perf_ind_enable, mt7925_perf_ind_enable_get,
+			 mt7925_perf_ind_enable_set, "%lld\n");
+
+static int
+mt7925_coex_info(struct seq_file *s, void *data)
+{
+#define MT7925_CHIP_CONFIG_RESP_SIZE	320
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u8 resp[MT7925_CHIP_CONFIG_RESP_SIZE], resp_type;
+	int i, ret;
+
+	mt792x_mutex_acquire(dev);
+	ret = mt7925_mcu_chip_config_query(dev, "coexBwcGetModeInfo 0",
+					   &resp_type, resp, sizeof(resp));
+	mt792x_mutex_release(dev);
+
+	if (ret < 0)
+		return ret;
+
+	if (!ret)
+		seq_puts(s, "no reply\n");
+	else if (resp_type == CHIP_CONFIG_TYPE_ASCII)
+		seq_printf(s, "%.*s\n", ret, resp);
+	else
+		for (i = 0; i < ret; i += 16)
+			seq_printf(s, "%04x: %*ph\n", i,
+				   min_t(int, 16, ret - i), resp + i);
+
+	return 0;
+}
+
+/* mt7925_dmashdl_read: standard DMASHDL (MT7925/MT7927 only).
+ * MT7928 uses DMASHDL Lite; see mt7928_dmashdl_lite_read below.
+ */
+static int
+mt7925_dmashdl_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u32 total_src = 0, total_rsv = 0;
+	u32 ffa_cnt, free_pg_cnt;
+	u32 ple_rsv, ple_src;
+	u32 val;
+	int i;
+
+	if (!mt76_is_mmio(&dev->mt76)) {
+		seq_puts(s, "DMASHDL dump not supported on this interface\n");
+		return 0;
+	}
+
+	mt792x_mutex_acquire(dev);
+
+	seq_puts(s, "DMASHDL Info:\n");
+
+	val = mt76_rr(dev, MT_DMASHDL_REFILL);
+	seq_printf(s, "\tRefill control(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_REFILL, val);
+
+	val = mt76_rr(dev, MT_DMASHDL_PKT_MAX_SIZE);
+	seq_printf(s, "\tPkt max size(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_PKT_MAX_SIZE, val);
+	seq_printf(s, "\t\tPLE pkt max=0x%03x, PSE pkt max=0x%03x\n",
+		   FIELD_GET(MT_DMASHDL_PKT_MAX_SIZE_PLE, val),
+		   FIELD_GET(MT_DMASHDL_PKT_MAX_SIZE_PSE, val));
+
+	val = mt76_rr(dev, MT_DMASHDL_ERROR_FLAG_CTRL);
+	seq_printf(s, "\tError flag ctrl(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_ERROR_FLAG_CTRL, val);
+
+	for (i = 0; i < 15; i++) {
+		u32 quota = mt76_rr(dev, MT_DMASHDL_GROUP_QUOTA(i));
+		u32 status = mt76_rr(dev, MT_DMASHDL_STATUS_RD_GP(i));
+		u32 rsv = FIELD_GET(MT_DMASHDL_STATUS_RD_GP_RSV, status);
+		u32 src = FIELD_GET(MT_DMASHDL_STATUS_RD_GP_SRC, status);
+
+		seq_printf(s, "\tGroup %2d: quota(min/max)=0x%03x/0x%03x, used/rsv=0x%03x/0x%03x\n",
+			   i,
+			   FIELD_GET(MT_DMASHDL_GROUP_QUOTA_MIN, quota),
+			   FIELD_GET(MT_DMASHDL_GROUP_QUOTA_MAX, quota),
+			   src, rsv);
+		total_src += src;
+		total_rsv += rsv;
+	}
+
+	val = mt76_rr(dev, MT_DMASHDL_STATUS_RD);
+	free_pg_cnt = FIELD_GET(MT_DMASHDL_STATUS_RD_FREE_PG, val);
+	ffa_cnt = FIELD_GET(MT_DMASHDL_STATUS_RD_FFA, val);
+	seq_printf(s, "\tStatus_RD(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_STATUS_RD, val);
+	seq_printf(s, "\t\tfree page cnt=0x%03x, ffa cnt=0x%03x\n",
+		   free_pg_cnt, ffa_cnt);
+
+	/* Counter mismatch check against PLE HIF page counters */
+	seq_puts(s, "\nDMASHDL Counter Check:\n");
+	val = mt76_rr(dev, MT_PLE_HIF_PG_INFO);
+	ple_rsv = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	ple_src = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	seq_printf(s, "\tPLE: used/rsv pages of HIF group=0x%03x/0x%03x\n",
+		   ple_src, ple_rsv);
+	seq_printf(s, "\tDMASHDL: total used pages of group0~14=0x%03x\n",
+		   total_src);
+	if (ple_src != total_src)
+		seq_puts(s, "\t[MISMATCH] PLE used pages != DMASHDL total used\n");
+	seq_printf(s, "\tDMASHDL: total rsv pages of group0~14=0x%03x\n",
+		   total_rsv);
+	seq_printf(s, "\tDMASHDL: total ffa pages=0x%03x, total free pages=0x%03x\n",
+		   ffa_cnt, free_pg_cnt);
+	if (free_pg_cnt != total_rsv + ffa_cnt)
+		seq_puts(s, "\t[MISMATCH] free pages != rsv + ffa\n");
+	if (free_pg_cnt != ple_rsv)
+		seq_puts(s, "\t[MISMATCH] free pages != PLE rsv pages\n");
+	if (ple_src == total_src && free_pg_cnt == total_rsv + ffa_cnt &&
+	    free_pg_cnt == ple_rsv)
+		seq_puts(s, "\tDMASHDL: no counter mismatch\n");
+
+	mt792x_mutex_release(dev);
+	return 0;
+}
+
+/* mt7928_dmashdl_lite_read: DMASHDL Lite (MT7928 only, base 0x20026200).
+ * Unlike full DMASHDL, Lite splits runtime state into separate registers:
+ *   GROUP_CONTROL (+0x100): min/max quota (config)
+ *   GROUP_SRC_CNT (+0x200): pages sourced (in-use) per group (runtime)
+ *   GROUP_ACK_CNT (+0x300): pages acknowledged (returned) per group (runtime)
+ *   GROUP_VALID   (+0x400): bitmap of groups with active page allocations
+ */
+static int
+mt7928_dmashdl_lite_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u32 disable0, disable1, valid0, valid1;
+	u32 total_src = 0, total_ack = 0;
+	u32 ple_rsv, ple_src;
+	u32 val;
+	int i;
+
+	if (!mt76_is_mmio(&dev->mt76)) {
+		seq_puts(s, "DMASHDL Lite dump not supported on this interface\n");
+		return 0;
+	}
+
+	mt792x_mutex_acquire(dev);
+
+	seq_puts(s, "DMASHDL Lite Info (MT7928):\n");
+
+	val = mt76_rr(dev, MT_DMASHDL_LITE_MAIN_CONTROL);
+	seq_printf(s, "\tMain control(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_MAIN_CONTROL, val);
+
+	val = mt76_rr(dev, MT_DMASHDL_LITE_PAGE_SIZE);
+	seq_printf(s, "\tPage size(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_PAGE_SIZE, val);
+	seq_printf(s, "\t\tPSE page size=0x%04x, PLE page size=0x%04x\n",
+		   FIELD_GET(MT_DMASHDL_LITE_PSE_PAGE_SIZE_MASK, val),
+		   FIELD_GET(MT_DMASHDL_LITE_PLE_PAGE_SIZE_MASK, val));
+
+	val = mt76_rr(dev, MT_DMASHDL_LITE_PKT_MAX_SIZE);
+	seq_printf(s, "\tPkt max size(0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_PKT_MAX_SIZE, val);
+	seq_printf(s, "\t\tPSE pkt max=0x%04x, PLE pkt max=0x%04x\n",
+		   FIELD_GET(MT_DMASHDL_LITE_PSE_PKT_MAX_SIZE_MASK, val),
+		   FIELD_GET(MT_DMASHDL_LITE_PLE_PKT_MAX_SIZE_MASK, val));
+
+	val = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_SN_CHK0);
+	seq_printf(s, "\tGroup SN check[31:0](0x%08x):  0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_SN_CHK0, val);
+	val = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_SN_CHK1);
+	seq_printf(s, "\tGroup SN check[63:32](0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_SN_CHK1, val);
+	val = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_UDF_CHK0);
+	seq_printf(s, "\tGroup UDF check[31:0](0x%08x):  0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_UDF_CHK0, val);
+	val = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_UDF_CHK1);
+	seq_printf(s, "\tGroup UDF check[63:32](0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_UDF_CHK1, val);
+
+	disable0 = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_DISABLE0);
+	seq_printf(s, "\tGroup disable[31:0](0x%08x):  0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_DISABLE0, disable0);
+	disable1 = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_DISABLE1);
+	seq_printf(s, "\tGroup disable[63:32](0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_DISABLE1, disable1);
+
+	valid0 = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_VALID_0_31);
+	seq_printf(s, "\tGroup valid[31:0](0x%08x):  0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_VALID_0_31, valid0);
+	valid1 = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_VALID_32_63);
+	seq_printf(s, "\tGroup valid[63:32](0x%08x): 0x%08x\n",
+		   MT_DMASHDL_LITE_GROUP_VALID_32_63, valid1);
+
+	seq_puts(s, "\tQ mapping (q2group):\n");
+	for (i = 0; i < 8; i++) {
+		val = mt76_rr(dev, MT_DMASHDL_LITE_Q_MAP(i));
+		seq_printf(s, "\t\tQ_MAP[%d](0x%08x): 0x%08x\n",
+			   i, MT_DMASHDL_LITE_Q_MAP(i), val);
+	}
+
+	seq_puts(s, "\tGroup status (skipping disabled+zero quota):\n");
+	for (i = 0; i < DMASHDL_LITE_GROUP_NUM; i++) {
+		bool enabled = !((i < 32 ? disable0 : disable1) >> (i % 32) & 1);
+		u32 quota, src, ack_reg;
+		u32 pktin, add_ret, ret;
+
+		quota = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_QUOTA(i));
+		if (!enabled && !quota)
+			continue;
+
+		src     = FIELD_GET(MT_DMASHDL_LITE_GROUP_SRC_CNT_MASK,
+				    mt76_rr(dev, MT_DMASHDL_LITE_GROUP_SRC_CNT(i)));
+		ack_reg = mt76_rr(dev, MT_DMASHDL_LITE_GROUP_ACK_CNT(i));
+		pktin   = FIELD_GET(MT_DMASHDL_LITE_ACK_PKTIN_CNT,   ack_reg);
+		add_ret = FIELD_GET(MT_DMASHDL_LITE_ACK_ADD_RET_CNT, ack_reg);
+		ret     = FIELD_GET(MT_DMASHDL_LITE_ACK_RET_CNT,     ack_reg);
+
+		seq_printf(s, "\tGroup %2d: quota(min/max)=0x%03x/0x%03x, src=0x%02x, ack(add_ret/pktin/ret)=0x%02x/0x%02x/0x%02x%s\n",
+			   i,
+			   FIELD_GET(MT_DMASHDL_LITE_GROUP_MIN_QUOTA_MASK, quota),
+			   FIELD_GET(MT_DMASHDL_LITE_GROUP_MAX_QUOTA_MASK, quota),
+			   src, add_ret, pktin, ret,
+			   enabled ? "" : " [disabled]");
+		if (enabled) {
+			total_src += src;
+			total_ack += ret;
+		}
+	}
+	seq_printf(s, "\tTotal (enabled groups): src=0x%03x, ack_ret=0x%03x\n",
+		   total_src, total_ack);
+
+	/* Counter mismatch check against PLE HIF page counters */
+	seq_puts(s, "\nDMASHDL Lite Counter Check:\n");
+	val = mt76_rr(dev, MT_PLE_HIF_PG_INFO);
+	ple_rsv = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	ple_src = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	seq_printf(s, "\tPLE: used/rsv pages of HIF group=0x%03x/0x%03x\n",
+		   ple_src, ple_rsv);
+	seq_printf(s, "\tDMASHDL Lite: total src=0x%03x, total ack_ret=0x%03x\n",
+		   total_src, total_ack);
+	if (ple_src != total_src)
+		seq_puts(s, "\t[MISMATCH] PLE used pages != DMASHDL Lite total src\n");
+	else
+		seq_puts(s, "\tDMASHDL Lite: no counter mismatch\n");
+
+	seq_puts(s, "\nPLE Page Counters:\n");
+
+	val = mt76_rr(dev, MT_PLE_FREEPG_CNT);
+	seq_printf(s, "\tPLE free pages(0x%08x): total=0x%03x, ffa=0x%03x\n",
+		   MT_PLE_FREEPG_CNT,
+		   FIELD_GET(MT_WF_FREEPG_FREE_CNT, val),
+		   FIELD_GET(MT_WF_FREEPG_FFA_CNT, val));
+
+	val = mt76_rr(dev, MT_PLE_HIF_WMTXD_PG_INFO);
+	seq_printf(s, "\tPLE WMTXD group(0x%08x):     used/rsv=0x%03x/0x%03x\n",
+		   MT_PLE_HIF_WMTXD_PG_INFO,
+		   FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val),
+		   FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val));
+
+	val = mt76_rr(dev, MT_PLE_HIF_TXCMD_PG_INFO);
+	seq_printf(s, "\tPLE HIF_TXCMD group(0x%08x): used/rsv=0x%03x/0x%03x\n",
+		   MT_PLE_HIF_TXCMD_PG_INFO,
+		   FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val),
+		   FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val));
+
+	val = mt76_rr(dev, MT_PLE_CPU_PG_INFO);
+	seq_printf(s, "\tPLE CPU group(0x%08x):       used/rsv=0x%03x/0x%03x\n",
+		   MT_PLE_CPU_PG_INFO,
+		   FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val),
+		   FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val));
+
+	mt792x_mutex_release(dev);
+	return 0;
+}
+
+static int
+mt7925_ple_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u32 pbuf_ctrl, val;
+	u32 fpg_cnt, ffa_cnt, fpg_head, fpg_tail;
+	u32 max_q, min_q, rsv_pg, used_pg;
+
+	if (!mt76_is_mmio(&dev->mt76)) {
+		seq_puts(s, "PLE dump not supported on this interface\n");
+		return 0;
+	}
+
+	mt792x_mutex_acquire(dev);
+
+	pbuf_ctrl = mt76_rr(dev, MT_PLE_PBUF_CTRL);
+	seq_puts(s, "PLE Configuration Info:\n");
+	seq_printf(s, "\tPacket Buffer Control(0x%08x): 0x%08x\n",
+		   MT_PLE_PBUF_CTRL, pbuf_ctrl);
+	seq_printf(s, "\t\tPage Size=%d(%d bytes/page), Offset=%d, Total=%d pages\n",
+		   (int)FIELD_GET(MT_WF_PBUF_PAGE_SIZE, pbuf_ctrl),
+		   FIELD_GET(MT_WF_PBUF_PAGE_SIZE, pbuf_ctrl) ? 128 : 64,
+		   (int)FIELD_GET(MT_WF_PBUF_PBUF_OFFSET, pbuf_ctrl),
+		   (int)FIELD_GET(MT_WF_PBUF_TOTAL_PAGES, pbuf_ctrl));
+
+	val = mt76_rr(dev, MT_PLE_INT_N9_ERR_STS);
+	seq_printf(s, "\tINT_N9_ERR_STS(0x%08x): 0x%08x\n",
+		   MT_PLE_INT_N9_ERR_STS, val);
+	val = mt76_rr(dev, MT_PLE_INT_N9_ERR_STS_1);
+	seq_printf(s, "\tINT_N9_ERR_STS_1(0x%08x): 0x%08x\n",
+		   MT_PLE_INT_N9_ERR_STS_1, val);
+
+	seq_puts(s, "PLE Page Flow Control:\n");
+
+	val = mt76_rr(dev, MT_PLE_FREEPG_CNT);
+	fpg_cnt = FIELD_GET(MT_WF_FREEPG_FREE_CNT, val);
+	ffa_cnt = FIELD_GET(MT_WF_FREEPG_FFA_CNT, val);
+	seq_printf(s, "\tFree page counter(0x%08x): 0x%08x\n",
+		   MT_PLE_FREEPG_CNT, val);
+	seq_printf(s, "\t\ttotal free=0x%03x, ffa=0x%03x\n", fpg_cnt, ffa_cnt);
+
+	val = mt76_rr(dev, MT_PLE_FREEPG_HEAD_TAIL);
+	fpg_head = FIELD_GET(MT_WF_FREEPG_HEAD, val);
+	fpg_tail = FIELD_GET(MT_WF_FREEPG_TAIL, val);
+	seq_printf(s, "\tFree page head/tail(0x%08x): 0x%08x\n",
+		   MT_PLE_FREEPG_HEAD_TAIL, val);
+	seq_printf(s, "\t\ttail/head=0x%03x/0x%03x\n", fpg_tail, fpg_head);
+
+	/* HIF group */
+	val = mt76_rr(dev, MT_PLE_PG_HIF_GROUP);
+	max_q = FIELD_GET(MT_WF_GROUP_QUOTA_MAX, val);
+	min_q = FIELD_GET(MT_WF_GROUP_QUOTA_MIN, val);
+	seq_printf(s, "\tHIF group quota(0x%08x): 0x%08x (max/min=0x%03x/0x%03x)\n",
+		   MT_PLE_PG_HIF_GROUP, val, max_q, min_q);
+	val = mt76_rr(dev, MT_PLE_HIF_PG_INFO);
+	used_pg = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	rsv_pg  = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	seq_printf(s, "\tHIF group status(0x%08x): 0x%08x (used/rsv=0x%03x/0x%03x)\n",
+		   MT_PLE_HIF_PG_INFO, val, used_pg, rsv_pg);
+
+	/* WMTXD group */
+	val = mt76_rr(dev, MT_PLE_PG_HIF_WMTXD_GROUP);
+	max_q = FIELD_GET(MT_WF_GROUP_QUOTA_MAX, val);
+	min_q = FIELD_GET(MT_WF_GROUP_QUOTA_MIN, val);
+	seq_printf(s, "\tWMTXD group quota(0x%08x): 0x%08x (max/min=0x%03x/0x%03x)\n",
+		   MT_PLE_PG_HIF_WMTXD_GROUP, val, max_q, min_q);
+	val = mt76_rr(dev, MT_PLE_HIF_WMTXD_PG_INFO);
+	used_pg = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	rsv_pg  = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	seq_printf(s, "\tWMTXD group status(0x%08x): 0x%08x (used/rsv=0x%03x/0x%03x)\n",
+		   MT_PLE_HIF_WMTXD_PG_INFO, val, used_pg, rsv_pg);
+
+	/* HIF_TXCMD group */
+	val = mt76_rr(dev, MT_PLE_PG_HIF_TXCMD_GROUP);
+	max_q = FIELD_GET(MT_WF_GROUP_QUOTA_MAX, val);
+	min_q = FIELD_GET(MT_WF_GROUP_QUOTA_MIN, val);
+	seq_printf(s, "\tHIF_TXCMD group quota(0x%08x): 0x%08x (max/min=0x%03x/0x%03x)\n",
+		   MT_PLE_PG_HIF_TXCMD_GROUP, val, max_q, min_q);
+	val = mt76_rr(dev, MT_PLE_HIF_TXCMD_PG_INFO);
+	used_pg = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	rsv_pg  = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	seq_printf(s, "\tHIF_TXCMD group status(0x%08x): 0x%08x (used/rsv=0x%03x/0x%03x)\n",
+		   MT_PLE_HIF_TXCMD_PG_INFO, val, used_pg, rsv_pg);
+
+	/* CPU group */
+	val = mt76_rr(dev, MT_PLE_PG_CPU_GROUP);
+	max_q = FIELD_GET(MT_WF_GROUP_QUOTA_MAX, val);
+	min_q = FIELD_GET(MT_WF_GROUP_QUOTA_MIN, val);
+	seq_printf(s, "\tCPU group quota(0x%08x): 0x%08x (max/min=0x%03x/0x%03x)\n",
+		   MT_PLE_PG_CPU_GROUP, val, max_q, min_q);
+	val = mt76_rr(dev, MT_PLE_CPU_PG_INFO);
+	used_pg = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);
+	rsv_pg  = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);
+	seq_printf(s, "\tCPU group status(0x%08x): 0x%08x (used/rsv=0x%03x/0x%03x)\n",
+		   MT_PLE_CPU_PG_INFO, val, used_pg, rsv_pg);
+
+	val = mt76_rr(dev, MT_PLE_QUEUE_EMPTY);
+	seq_printf(s, "\tQueue empty(0x%08x): 0x%08x%s\n",
+		   MT_PLE_QUEUE_EMPTY, val,
+		   (val & MT_PLE_QUEUE_EMPTY_ALL_AC) ? " [ALL AC EMPTY]" : "");
+
+	mt792x_mutex_release(dev);
+	return 0;
+}
+
+static int
+mt7925_pse_read(struct seq_file *s, void *data)
+{
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	u32 pbuf_ctrl, val;
+	u32 fpg_cnt, ffa_cnt, fpg_head, fpg_tail;
+	u32 max_q, min_q, rsv_pg, used_pg;
+
+	if (!mt76_is_mmio(&dev->mt76)) {
+		seq_puts(s, "PSE dump not supported on this interface\n");
+		return 0;
+	}
+
+	mt792x_mutex_acquire(dev);
+
+	pbuf_ctrl = mt76_rr(dev, MT_PSE_PBUF_CTRL);
+	seq_puts(s, "PSE Configuration Info:\n");
+	seq_printf(s, "\tPacket Buffer Control(0x%08x): 0x%08x\n",
+		   MT_PSE_PBUF_CTRL, pbuf_ctrl);
+	seq_printf(s, "\t\tPage Size=%d(%d bytes/page), Offset=%d, Total=%d pages\n",
+		   (int)FIELD_GET(MT_WF_PBUF_PAGE_SIZE, pbuf_ctrl),
+		   FIELD_GET(MT_WF_PBUF_PAGE_SIZE, pbuf_ctrl) ? 256 : 128,
+		   (int)FIELD_GET(MT_WF_PBUF_PBUF_OFFSET, pbuf_ctrl),
+		   (int)FIELD_GET(MT_WF_PBUF_TOTAL_PAGES, pbuf_ctrl));
+
+	val = mt76_rr(dev, MT_PSE_INT_N9_ERR_STS);
+	seq_printf(s, "\tINT_N9_ERR_STS(0x%08x): 0x%08x\n",
+		   MT_PSE_INT_N9_ERR_STS, val);
+	val = mt76_rr(dev, MT_PSE_INT_N9_ERR1_STS);
+	seq_printf(s, "\tINT_N9_ERR1_STS(0x%08x): 0x%08x\n",
+		   MT_PSE_INT_N9_ERR1_STS, val);
+
+	val = mt76_rr(dev, MT_PSE_QUEUE_EMPTY);
+	seq_printf(s, "\tQueue empty(0x%08x): 0x%08x\n",
+		   MT_PSE_QUEUE_EMPTY, val);
+
+	seq_puts(s, "PSE Page Flow Control:\n");
+
+	val = mt76_rr(dev, MT_PSE_FREEPG_CNT);
+	fpg_cnt = FIELD_GET(MT_WF_FREEPG_FREE_CNT, val);
+	ffa_cnt = FIELD_GET(MT_WF_FREEPG_FFA_CNT, val);
+	seq_printf(s, "\tFree page counter(0x%08x): 0x%08x\n",
+		   MT_PSE_FREEPG_CNT, val);
+	seq_printf(s, "\t\ttotal free=0x%03x, ffa=0x%03x\n", fpg_cnt, ffa_cnt);
+
+	val = mt76_rr(dev, MT_PSE_FREEPG_HEAD_TAIL);
+	fpg_head = FIELD_GET(MT_WF_FREEPG_HEAD, val);
+	fpg_tail = FIELD_GET(MT_WF_FREEPG_TAIL, val);
+	seq_printf(s, "\tFree page head/tail(0x%08x): 0x%08x\n",
+		   MT_PSE_FREEPG_HEAD_TAIL, val);
+	seq_printf(s, "\t\ttail/head=0x%03x/0x%03x\n", fpg_tail, fpg_head);
+
+#define MT7925_PSE_GROUP(_name, _qreg, _ireg)					\
+do {										\
+	val = mt76_rr(dev, _qreg);						\
+	max_q = FIELD_GET(MT_WF_GROUP_QUOTA_MAX, val);				\
+	min_q = FIELD_GET(MT_WF_GROUP_QUOTA_MIN, val);				\
+	seq_printf(s, "\t" _name " group quota(0x%08x): 0x%08x"		\
+		   " (max/min=0x%03x/0x%03x)\n", _qreg, val, max_q, min_q);	\
+	val = mt76_rr(dev, _ireg);						\
+	used_pg = FIELD_GET(MT_WF_PG_INFO_SRC_CNT, val);			\
+	rsv_pg  = FIELD_GET(MT_WF_PG_INFO_RSV_CNT, val);			\
+	seq_printf(s, "\t" _name " group status(0x%08x): 0x%08x"		\
+		   " (used/rsv=0x%03x/0x%03x)\n", _ireg, val, used_pg, rsv_pg);\
+} while (0)
+
+	MT7925_PSE_GROUP("HIF0",  MT_PSE_PG_HIF0_GROUP,  MT_PSE_HIF0_PG_INFO);
+	MT7925_PSE_GROUP("HIF1",  MT_PSE_PG_HIF1_GROUP,  MT_PSE_HIF1_PG_INFO);
+	MT7925_PSE_GROUP("HIF2",  MT_PSE_PG_HIF2_GROUP,  MT_PSE_HIF2_PG_INFO);
+	MT7925_PSE_GROUP("CPU",   MT_PSE_PG_CPU_GROUP,   MT_PSE_CPU_PG_INFO);
+	MT7925_PSE_GROUP("PLE",   MT_PSE_PG_PLE_GROUP,   MT_PSE_PLE_PG_INFO);
+	MT7925_PSE_GROUP("LMAC0", MT_PSE_PG_LMAC0_GROUP, MT_PSE_LMAC0_PG_INFO);
+	MT7925_PSE_GROUP("LMAC1", MT_PSE_PG_LMAC1_GROUP, MT_PSE_LMAC1_PG_INFO);
+	MT7925_PSE_GROUP("LMAC2", MT_PSE_PG_LMAC2_GROUP, MT_PSE_LMAC2_PG_INFO);
+	MT7925_PSE_GROUP("LMAC3", MT_PSE_PG_LMAC3_GROUP, MT_PSE_LMAC3_PG_INFO);
+	MT7925_PSE_GROUP("MDP",   MT_PSE_PG_MDP_GROUP,   MT_PSE_MDP_PG_INFO);
+
+#undef MT7925_PSE_GROUP
+
+	mt792x_mutex_release(dev);
+	return 0;
+}
+
+static const u32 stats_query_mib_counters[] = {
+	UNI_CMD_MIB_CNT_RX_FCS_ERR,
+	UNI_CMD_MIB_CNT_RX_FIFO_OVERFLOW,
+	UNI_CMD_MIB_CNT_RX_MPDU,
+	UNI_CMD_MIB_CNT_AMPDU_RX_COUNT,
+	UNI_CMD_MIB_CNT_PF_DROP,
+	UNI_CMD_MIB_CNT_LEN_MISMATCH,
+	UNI_CMD_MIB_CNT_CHANNEL_IDLE,
+	UNI_CMD_MIB_CNT_CCA_NAV_TX_TIME,
+	UNI_CMD_MIB_CNT_MDRDY,
+	UNI_CMD_MIB_CNT_RX_CCK_MDRDY_TIME,
+	UNI_CMD_MIB_CNT_RX_OFDM_LG_MIXED_MDRDY_TIME,
+	UNI_CMD_MIB_CNT_RX_OFDM_GREEN_MDRDY_TIME,
+	UNI_CMD_MIB_CNT_P_CCA_TIME,
+	UNI_CMD_MIB_CNT_S_CCA_TIME,
+	UNI_CMD_MIB_CNT_P_ED_TIME,
+	UNI_CMD_MIB_CNT_BCN_TX,
+	UNI_CMD_MIB_CNT_TX_BW_40MHZ,
+	UNI_CMD_MIB_CNT_TX_BW_80MHZ,
+	UNI_CMD_MIB_CNT_TX_BW_160MHZ,
+	UNI_CMD_MIB_CNT_BSS0_BA_MISS,
+	UNI_CMD_MIB_CNT_BSS0_RTS_TX_CNT,
+	UNI_CMD_MIB_CNT_BSS0_FRAME_RETRY,
+	UNI_CMD_MIB_CNT_BSS0_FRAME_RETRY_2,
+	UNI_CMD_MIB_CNT_BSS0_RTS_RETRY,
+	UNI_CMD_MIB_CNT_BSS0_ACK_FAIL,
+	UNI_CMD_MIB_CNT_AMPDU,
+	UNI_CMD_MIB_CNT_AMPDU_MPDU,
+	UNI_CMD_MIB_CNT_AMPDU_ACKED,
+};
+
+/* Dump the whole firmware auto-rate table and mark the entry currently in use
+ * with "-->"
+ */
+static void
+mt7925_dump_auto_rate_table(struct seq_file *s,
+			    const struct mt7925_wtbl_rate *wr)
+{
+	u8 fcap = wr->fcap;
+	u8 rate_idx = wr->rate_idx;
+	u8 cbrn = wr->cbrn;
+	int i;
+
+	seq_printf(s, "%-22s  Cur Rate Index = %u\n", " ", rate_idx);
+
+	for (i = 0; i < MT7925_AUTO_RATE_NUM; i++) {
+		u16 rate_code = le16_to_cpu(wr->rate_code[i]);
+		u8 txmode = MT7925_HW_RATE_TO_MODE(rate_code);
+		u8 rate = MT7925_HW_RATE_TO_MCS(rate_code);
+		u8 nsts = MT7925_HW_RATE_TO_NSS(rate_code) + 1;
+		u8 stbc = MT7925_HW_RATE_TO_STBC(rate_code);
+		u8 dcm = MT7925_HW_RATE_TO_DCM(rate_code);
+		u8 ersu106t = MT7925_HW_RATE_TO_106T(rate_code);
+		u8 sgi = mt7925_wtbl_get_sgi(wr, txmode, fcap);
+		u8 ldpc = mt7925_wtbl_get_ldpc(wr, txmode);
+		const char *mode_str = "N/A";
+
+		if (dcm)
+			rate = MT7925_HW_RATE_UNMASK_DCM(rate);
+		if (ersu106t)
+			rate = MT7925_HW_RATE_UNMASK_106T(rate);
+
+		if (txmode < ARRAY_SIZE(mt7925_tx_mode_str))
+			mode_str = mt7925_tx_mode_str[txmode];
+
+		seq_printf(s, "Rate index[%d]    %s", i,
+			   rate_idx == i ? "--> " : "    ");
+
+		/* rate/MCS field */
+		if (txmode == MT7925_TX_RATE_MODE_CCK)
+			seq_printf(s, "%s, ",
+				   mt7925_tx_rate_cck_str[rate & 0x3]);
+		else if (txmode == MT7925_TX_RATE_MODE_OFDM)
+			seq_printf(s, "%s, ", mt7925_hw_rate_ofdm_str(rate));
+		else if (txmode == MT7925_TX_RATE_MODE_HTMIX ||
+			 txmode == MT7925_TX_RATE_MODE_HTGF)
+			seq_printf(s, "MCS%d, ", rate);
+		else
+			seq_printf(s, "%s%d_MCS%d, ",
+				   stbc ? "NSTS" : "NSS", nsts, rate);
+
+		/* bandwidth: mirror the CCK/OFDM -> BW20 and cbrn handling */
+		if (txmode == MT7925_TX_RATE_MODE_CCK ||
+		    txmode == MT7925_TX_RATE_MODE_OFDM)
+			seq_printf(s, "%s, ", mt7925_tx_rate_bw_str[0]);
+		else if (i > cbrn)
+			seq_printf(s, "%s, ",
+				   fcap < 5 ?
+				   (fcap > MT7925_BW_20 ?
+				    mt7925_tx_rate_bw_str[fcap - 1] :
+				    mt7925_tx_rate_bw_str[fcap]) :
+				   mt7925_tx_rate_bw_str[5]);
+		else
+			seq_printf(s, "%s, ",
+				   fcap < 5 ? mt7925_tx_rate_bw_str[fcap] :
+				   mt7925_tx_rate_bw_str[5]);
+
+		/* GI / preamble */
+		if (txmode == MT7925_TX_RATE_MODE_CCK)
+			seq_printf(s, "%s, ", rate < 4 ? "LP" : "SP");
+		else if (txmode == MT7925_TX_RATE_MODE_OFDM)
+			; /* OFDM has no GI/preamble to print */
+		else if (txmode == MT7925_TX_RATE_MODE_HTMIX ||
+			 txmode == MT7925_TX_RATE_MODE_HTGF ||
+			 txmode == MT7925_TX_RATE_MODE_VHT ||
+			 txmode == MT7925_TX_RATE_MODE_PLR)
+			seq_printf(s, "%s, ", sgi == 0 ? "LGI" : "SGI");
+		else
+			seq_printf(s, "%s, ",
+				   sgi == 0 ? "SGI" :
+				   (sgi == 1 ? "MGI" : "LGI"));
+
+		/* mode, DCM, 106t, STBC, coding */
+		seq_printf(s, "%s%s%s%s%s\n",
+			   mode_str,
+			   dcm ? ", DCM" : "", ersu106t ? ", 106t" : "",
+			   stbc ? ", STBC, " : ", ",
+			   (ldpc == 0 ||
+			    txmode == MT7925_TX_RATE_MODE_CCK ||
+			    txmode == MT7925_TX_RATE_MODE_OFDM) ?
+			   "BCC" : "LDPC");
+	}
+}
+
+static int mt792x_stats(struct seq_file *s, void *data)
+{
+	s8 rssi = 0;
+	u8 band_idx = 0;
+	bool wtbl_rate_valid = false;
+	int ret = 0;
+	int i = 0;
+	u32 stats_value = 0;
+	u32 tx_link_speed_kbps = 0;
+	u64 tx_total = 0;
+	u64 tx_fail = 0;
+	u64 tx_per = 0;
+	/* UNI_CMD_MIB_DATA */
+	u64 mib_values[ARRAY_SIZE(stats_query_mib_counters)];
+	struct mt792x_dev *dev = dev_get_drvdata(s->private);
+	struct mt7925_wtbl_rate wtbl_rate;
+	struct mt7925_sta_stats *stats = NULL;
+
+	band_idx = dev->mphy.band_idx;
+
+	 stats = kzalloc(sizeof(*stats), GFP_KERNEL);
+	if (!stats)
+		return -ENOMEM;
+
+	/* Get STA_STATS */
+	mt792x_mutex_acquire(dev);
+	ret = mt792x_mcu_get_stat(dev, stats);
+	mt792x_mutex_release(dev);
+	if (ret)
+		goto out;
+
+	/* Get MIB_STATS */
+	mt792x_mutex_acquire(dev);
+	ret = mt7925_mcu_get_mib_info(dev, band_idx, stats_query_mib_counters,
+				      ARRAY_SIZE(stats_query_mib_counters), mib_values);
+	mt792x_mutex_release(dev);
+	if (ret)
+		goto out;
+
+	/* Get Link_Speed and RSSI */
+	mt792x_mutex_acquire(dev);
+	ret = mt7925_mcu_get_link_quality(dev, stats->bss_idx, &rssi, &tx_link_speed_kbps);
+	mt792x_mutex_release(dev);
+	if (ret)
+		goto out;
+
+	/* Get the firmware auto-rate table (best effort; do not abort the
+	 * whole stats dump if the WTBL query is not available).
+	 */
+	mt792x_mutex_acquire(dev);
+	if (!mt792x_mcu_get_wtbl_rate(dev, stats->wtbl_idx, &wtbl_rate))
+		wtbl_rate_valid = true;
+	mt792x_mutex_release(dev);
+
+	/* Print STA_STATS info */
+	seq_printf(s, "(STA) connected AP MAC Address = %pM\n", stats->mac_addr);
+	seq_printf(s, "%-22s  BssIdx = [%u]\n", " ", stats->bss_idx);
+	seq_printf(s, "%-22s  StaRecIdx = [%u]\n",
+		   " ", stats->sta_idx);
+	seq_printf(s, "%-22s  RSSI = %d\n", " ", rssi);
+	seq_printf(s, "%-22s  Link_Speed = %u (kbit/s)\n", " ", tx_link_speed_kbps);
+
+	seq_printf(s, "%-22s  temperature = %u\n",
+		   " ", stats->temperature);
+
+	stats_value = le32_to_cpu(stats->transmit_count) - le32_to_cpu(stats->transmit_fail_count);
+	seq_printf(s, "%-22s  Tx success = %u\n",
+		   " ", stats_value);
+
+	stats_value = le32_to_cpu(stats->tx_fail_count) - le32_to_cpu(stats->tx_life_timeout_count);
+	seq_printf(s, "%-22s  Tx fail to Rcv ACK after retry = %u\n",
+		   " ", stats_value);
+
+	seq_printf(s, "%-22s  Rx Mpdu = %u\n",
+		   " ", le32_to_cpu(stats->mib[band_idx].rx_mpdu_cnt));
+
+	seq_printf(s, "%-22s  Rx Fcs Error = %u (MPDU)\n",
+		   " ", le32_to_cpu(stats->mib[band_idx].fcs_error));
+	seq_printf(s, "%-22s  Rx FIFO full = %u (MPDU)\n",
+		   " ", le32_to_cpu(stats->mib[band_idx].rx_fifo_full));
+
+	for (i = 0; i < STAT_MIB_CNT_TRX_AGG_RANGE_MAX_NUM; i++)
+		seq_printf(s, "%-22s  TRX_AGG_RANGE[%d] = %u (PPDU)\n",
+			   " ", i, le32_to_cpu(stats->mib[band_idx].tx_range_ampdu_cnt[i]));
+
+	seq_printf(s, "%-22s  MPDUs in AMPDUs transmitted = %u (MPDU)\n",
+		   " ", le32_to_cpu(stats->mib[band_idx].ampdu_tx_sf_cnt));
+	seq_printf(s, "%-22s  MPDUs in AMPDUs transmitted with ACK reply = %u (MPDU)\n",
+		   " ", le32_to_cpu(stats->mib[band_idx].ampdu_tx_ack_sf_cnt));
+	seq_printf(s, "%-22s  rate1_tx_cnt = %u\n",
+		   " ", le32_to_cpu(stats->rate1_tx_cnt));
+	seq_printf(s, "%-22s  rate1_fail_cnt = %u\n",
+		   " ", le32_to_cpu(stats->rate1_fail_cnt));
+	seq_printf(s, "%-22s  train_up = %u\n",
+		   " ", le16_to_cpu(stats->train_up));
+	seq_printf(s, "%-22s  train_down = %u\n",
+		   " ", le16_to_cpu(stats->train_down));
+	seq_printf(s, "%-22s  is_force_tx_stream = %u\n",
+		   " ", stats->is_force_tx_stream);
+	seq_printf(s, "%-22s  is_force_se_off = %u\n",
+		   " ", stats->is_force_se_off);
+	seq_printf(s, "%-22s  max_ampdu_factor = %u\n",
+		   " ", stats->max_ampdu_factor);
+	seq_printf(s, "%-22s  tx_rate_up_penalty = %u\n",
+		   " ", stats->tx_rate_up_penalty);
+	seq_printf(s, "%-22s  low_traffic_mode = %u\n",
+		   " ", stats->low_traffic_mode);
+	seq_printf(s, "%-22s  low_traffic_count = %u\n",
+		   " ", stats->low_traffic_count);
+	seq_printf(s, "%-22s  low_traffic_dashboard = %u\n",
+		   " ", stats->low_traffic_dashboard);
+	seq_printf(s, "%-22s  dynamic_sgi_state = %u\n",
+		   " ", stats->dynamic_sgi_state);
+	seq_printf(s, "%-22s  dynamic_sgi_score = %u\n",
+		   " ", stats->dynamic_sgi_score);
+	seq_printf(s, "%-22s  dynamic_bw_state = %u\n",
+		   " ", stats->dynamic_bw_state);
+	seq_printf(s, "%-22s  dynamic_gband_256qam_state = %u\n",
+		   " ", stats->dynamic_gband_256qam_state);
+	seq_printf(s, "%-22s  vht_non_sp_rate_state = %u\n",
+		   " ", stats->vht_non_sp_rate_state);
+
+	/* Decode the TX Vector BBP latch to show the current TX MCS rate */
+	if (band_idx < MT7925_STA_STATS_BAND_NUM)
+		mt7925_print_last_tx_rate(s, stats->tx_vector[band_idx].txv);
+
+	/* Dump the whole firmware auto-rate table and mark the entry currently */
+	if (wtbl_rate_valid)
+		mt7925_dump_auto_rate_table(s, &wtbl_rate);
+
+	seq_puts(s, "\nmib state:\n");
+	/* ===Rx Related Counters=== */
+	seq_puts(s, "=== Rx Related Counters ===\n");
+	seq_printf(s, "%-22s  Rx with CRC = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_FCS_ERR]);
+	seq_printf(s, "%-22s  Rx drop due to out of resource = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_FIFO_OVERFLOW]);
+	seq_printf(s, "%-22s  Rx Mpdu = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_MPDU]);
+	seq_printf(s, "%-22s  Rx AMpdu = %llu (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_AMPDU_RX_COUNT]);
+	seq_printf(s, "%-22s  Rx PF Drop = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_PF_DROP]);
+	seq_printf(s, "%-22s  Rx Len Mismatch = %llu (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_LEN_MISMATCH]);
+
+	/* ===Phy/Timing Related Counters=== */
+	seq_puts(s, "\n=== Phy/Timing Related Counters ===\n");
+	seq_printf(s, "%-22s  ChannelIdleCnt = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_CHANNEL_IDLE]);
+	seq_printf(s, "%-22s  CCA_NAV_Tx_Time = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_CCA_NAV_TX_TIME]);
+	seq_printf(s, "%-22s  Rx_MDRDY_CNT = %llu (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_MDRDY]);
+	seq_printf(s, "%-22s  CCK_MDRDY = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_CCK_MDRDY_TIME]);
+	seq_printf(s, "%-22s  OFDM_MDRDY = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_OFDM_LG_MIXED_MDRDY_TIME]);
+	seq_printf(s, "%-22s  OFDM_GREEN_MDRDY = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_RX_OFDM_GREEN_MDRDY_TIME]);
+	seq_printf(s, "%-22s  Prim CCA Time = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_P_CCA_TIME]);
+	seq_printf(s, "%-22s  Sec CCA Time = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_S_CCA_TIME]);
+	seq_printf(s, "%-22s  Prim ED Time = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_P_ED_TIME]);
+
+	/* ===Tx Related Counters(Generic)=== */
+	seq_puts(s, "\n=== Tx Related Counters(Generic) ===\n");
+	seq_printf(s, "%-22s  BeaconTxCnt = %llu\n",
+		   " ", mib_values[QUERY_MIB_CNT_BCN_TX]);
+	seq_printf(s, "%-22s  Tx 40MHz Cnt = %llu  (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_TX_BW_40MHZ]);
+	seq_printf(s, "%-22s  Tx 80MHz Cnt = %llu  (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_TX_BW_80MHZ]);
+	seq_printf(s, "%-22s  Tx 160MHz Cnt = %llu  (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_TX_BW_160MHZ]);
+
+	/* ===BSSID[0] Related Counters=== */
+	seq_puts(s, "\n=== BSSID[0] Related Counters ===\n");
+	seq_printf(s, "%-22s  BA Miss Cnt = %llu  (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_BA_MISS]);
+	seq_printf(s, "%-22s  RTS Tx Cnt = %llu  (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_RTS_TX_CNT]);
+	seq_printf(s, "%-22s  Frame Retry Cnt = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_FRAME_RETRY]);
+	seq_printf(s, "%-22s  Frame Retry 2 Cnt = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_FRAME_RETRY_2]);
+	seq_printf(s, "%-22s  RTS Retry Cnt = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_RTS_RETRY]);
+	seq_printf(s, "%-22s  Ack Failed Cnt = %llu (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_BSS0_ACK_FAIL]);
+
+	/* ===AMPDU Related Counters=== */
+	seq_puts(s, "\n=== AMPDU Related Counters ===\n");
+	seq_printf(s, "%-22s  Tx AMPDU_Pkt_Cnt = %llu  (PPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_AMPDU]);
+	seq_printf(s, "%-22s  Tx AMPDU_MPDU_Pkt_Cnt = %llu  (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_AMPDU_MPDU]);
+	seq_printf(s, "%-22s  AMPDU Tx success = %llu (MPDU)\n",
+		   " ", mib_values[QUERY_MIB_CNT_AMPDU_ACKED]);
+
+	tx_total = mib_values[QUERY_MIB_CNT_AMPDU_MPDU];
+	tx_fail =  mib_values[QUERY_MIB_CNT_AMPDU_MPDU] - mib_values[QUERY_MIB_CNT_AMPDU_ACKED];
+	tx_per = tx_total == 0 ? 0 : (1000 * (tx_fail)) / tx_total;
+	seq_printf(s, "%-22s  AMPDU Tx fail count   = %llu  (MPDU), PER=%llu.%1llu%%\n",
+		   " ",
+		   tx_fail,
+		   tx_per / 10, tx_per % 10);
+
+out:
+	kfree(stats);
+	return ret;
+}
+
 int mt7925_init_debugfs(struct mt792x_dev *dev)
 {
 	struct dentry *dir;
@@ -313,5 +1365,7 @@ int mt7925_init_debugfs(struct mt792x_dev *dev)
 				    mt792x_pm_stats);
 	debugfs_create_file("deep-sleep", 0600, dir, dev, &fops_ds);
 
+	debugfs_create_devm_seqfile(dev->mt76.dev, "stats", dir,
+				    mt792x_stats);
 	return 0;
 }
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/stats.c b/drivers/net/wireless/mediatek/mt76/mt7925/stats.c
new file mode 100644
index 000000000000..9a47953062bc
--- /dev/null
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/stats.c
@@ -0,0 +1,585 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+/* Copyright (C) 2026 MediaTek Inc. */
+
+#include <linux/firmware.h>
+#include <linux/fs.h>
+#include "mt7925.h"
+#include "mcu.h"
+#include "mac.h"
+#include "stats.h"
+
+#define MT7925_STA_STATS_VERSION 1
+#define MT7925_STA_STATS_VALID 1U
+
+#define MT7925_MIB_MAX_CNT			32
+#define MT7925_MIB_CNT_MAX_NUM		512  /* firmware UNI_CMD_MIB_CNT_MAX_NUM upper bound */
+
+#define MT7925_LQ_BAND_NUM	4			/* Link quality band */
+
+const char * const mt7925_tx_mode_str[] = {
+	"CCK", "OFDM", "MM", "GF", "VHT", "PLR",
+	"PLRP", "ALR", "HE_SU", "HE_ER", "HE_TRIG", "HE_MU",
+	"EHT_ER", "TOF", "EHT_TRIG", "EHT_MU"
+};
+
+const char * const mt7925_tx_rate_cck_str[] = {
+	"1M", "2M", "5.5M", "11M", "N/A"
+};
+
+const char * const mt7925_tx_rate_bw_str[] = {
+	"BW20", "BW40", "BW80", "BW160/BW8080", "BW320", "N/A"
+};
+
+struct mt7925_sta_stats_req {
+	u8 rsv[4];
+	__le16 tag;
+	__le16 len;
+	u8 sta_rec_idx;
+	u8 rsv1;
+	u8 lls_read_clear;
+	u8 reset_counter;
+} __packed;
+
+struct mt7925_sta_stats_event {
+	u8 rsv[4];
+	__le16 tag;
+	__le16 len;
+	struct mt7925_sta_stats stats;
+} __packed;
+
+/* WTBL auto-rate request TLV, mirror Gen4m struct UNI_CMD_STAT_WTBL.
+ * The RA tag reuses UNI_CMD_ID_STATISTICS_WTBL (10).
+ */
+struct mt7925_wtbl_rate_req {
+	u8 rsv[4];
+	__le16 tag;
+	__le16 len;
+	__le16 wlan_idx;
+	__le16 rsv1;
+} __packed;
+
+/* WTBL auto-rate event TLV. Firmware answers cmd tag 10 with event tag 12. */
+struct mt7925_wtbl_rate_event {
+	u8 rsv[4];
+	struct mt7925_wtbl_rate rate;
+} __packed;
+
+/* fixed field, mirror UNI_(CMD|EVENT)_MIB_INFO */
+struct mt7925_mib_hdr {
+	u8 band_idx;
+	u8 rsv[3];
+} __packed;
+
+/* One requested/returned MIB counter, mirror UNI_(CMD|EVENT)_MIB_DATA_T. */
+struct mt7925_mib_data {
+	__le16 tag;	/* UNI_(CMD|EVENT)_MIB_DATA = 0 */
+	__le16 len;
+	__le32 counter;	/* MIB counter ID (enum mt7925_mib_counter) */
+	__le64 data;	/* only valid on event; ignored on cmd */
+} __packed;
+
+/* per-band link quality entry, mirror UNI_LINK_QUALITY */
+struct mt7925_link_quality {
+	s8 rssi;		/* moving-average beacon RSSI of connected AP */
+	s8 link_quality;	/* always 0 */
+	__le16 link_speed;	/* TX rate1, unit 500 Kb/s */
+	u8 medium_busy_pct;	/* always 0 */
+	u8 is_ready;		/* 0: fields invalid, 1: valid */
+	u8 rsv[2];
+} __packed;
+
+/* request TLV, mirror UNI_CMD_LINK_QUALITY (header + tag/len only) */
+struct mt7925_lq_req {
+	u8 rsv[4];		/* UNI_CMD_GET_STATISTICS fixed field */
+	__le16 tag;		/* UNI_CMD_GET_STATISTICS_TAG_LINK_QUALITY = 1 */
+	__le16 len;
+} __packed;
+
+/* event TLV, mirror UNI_EVENT_LINK_QUALITY */
+struct mt7925_lq_event {
+	u8 rsv[4];		/* UNI_EVENT_STATISTICS fixed field */
+	__le16 tag;		/* UNI_EVENT_ID_STATISTICS_LINK_QUALITY = 1 */
+	__le16 len;
+	struct mt7925_link_quality lq[MT7925_LQ_BAND_NUM];
+} __packed;
+
+static void
+mt7925_get_ap_sta_rec_idx_iter(void *data, u8 *mac,
+			       struct ieee80211_vif *vif)
+{
+	struct mt7925_sta_rec_lookup *lookup = data;
+	struct mt792x_vif *mvif;
+	struct mt792x_sta *msta;
+
+	if (lookup->sta_rec_idx >= 0)
+		return;
+
+	if (vif->type != NL80211_IFTYPE_STATION || !vif->cfg.assoc)
+		return;
+
+	mvif = (struct mt792x_vif *)vif->drv_priv;
+	msta = mvif->wep_sta;
+	if (!msta)
+		return;
+
+	if (!msta->deflink.wcid.sta)
+		return;
+
+	lookup->sta_rec_idx = msta->deflink.wcid.idx;
+}
+
+int
+mt7925_get_ap_sta_rec_idx(struct mt792x_dev *dev, u8 *sta_rec_idx)
+{
+	struct mt7925_sta_rec_lookup lookup = {
+		.sta_rec_idx = -1,
+	};
+
+	lockdep_assert_held(&dev->mt76.mutex);
+
+	ieee80211_iterate_active_interfaces(dev->mphy.hw,
+					    IEEE80211_IFACE_ITER_RESUME_ALL,
+					    mt7925_get_ap_sta_rec_idx_iter,
+					    &lookup);
+
+	if (lookup.sta_rec_idx < 0)
+		return -ENOTCONN;
+
+	if (lookup.sta_rec_idx > U8_MAX)
+		return -ERANGE;
+
+	*sta_rec_idx = lookup.sta_rec_idx;
+
+	return 0;
+}
+
+/* Decode the TX Vector BBP latch and print the last TX rate */
+void
+mt7925_print_last_tx_rate(struct seq_file *s, const __le32 le_txv[3])
+{
+	const char *mode_str = "N/A";
+	u8 rate, txmode, frmode, sgi, ldpc, nsts, stbc, spe, dcm, ersu106t;
+	s8 txpwr, pos_txpwr;
+	u32 txv[3];
+
+	txv[0] = le32_to_cpu(le_txv[0]);
+	txv[1] = le32_to_cpu(le_txv[1]);
+	txv[2] = le32_to_cpu(le_txv[2]);
+
+	if (txv[0] == 0xffffffff) {
+		seq_printf(s, "%-22s  %s = %s\n", " ", "Last TX Rate", "N/A");
+		seq_printf(s, "%-22s  %s = %s\n",
+			   " ", "Chip Out TX Power", "N/A");
+		return;
+	}
+
+	rate = MT7925_TXV_GET_TX_RATE(txv);
+	txmode = MT7925_TXV_GET_TX_MODE(txv);
+	frmode = MT7925_TXV_GET_TX_FRMODE(txv);
+	nsts = MT7925_TXV_GET_TX_NSTS(txv) + 1;
+	sgi = MT7925_TXV_GET_TX_SGI(txv);
+	ldpc = MT7925_TXV_GET_TX_LDPC(txv);
+	stbc = MT7925_TXV_GET_TX_STBC(txv);
+	txpwr = MT7925_TXV_GET_TX_PWR(txv);
+	spe = MT7925_TXV_GET_TX_SPE_IDX(txv);
+	dcm = MT7925_TXV_GET_TX_DCM(txv);
+	ersu106t = MT7925_TXV_GET_TX_106T(txv);
+
+	if (dcm)
+		rate = MT7925_TXV_RATE_UNMASK_DCM(rate);
+	if (ersu106t)
+		rate = MT7925_TXV_RATE_UNMASK_106T(rate);
+
+	if (txmode < ARRAY_SIZE(mt7925_tx_mode_str))
+		mode_str = mt7925_tx_mode_str[txmode];
+
+	seq_printf(s, "%-22s  %s = ", " ", "Last TX Rate");
+
+	/* rate/MCS field */
+	if (txmode == MT7925_TX_RATE_MODE_CCK)
+		seq_printf(s, "%s, ",
+			   rate < 4 ? mt7925_tx_rate_cck_str[rate] :
+			   (((rate >= 5) && (rate <= 7)) ?
+			    mt7925_tx_rate_cck_str[rate - 4] :
+			    mt7925_tx_rate_cck_str[4]));
+	else if (txmode == MT7925_TX_RATE_MODE_OFDM)
+		seq_printf(s, "%s, ", mt7925_hw_rate_ofdm_str(rate));
+	else if (txmode == MT7925_TX_RATE_MODE_HTMIX ||
+		 txmode == MT7925_TX_RATE_MODE_HTGF)
+		seq_printf(s, "MCS%d, ", rate);
+	else
+		seq_printf(s, "%s%d_MCS%d, ",
+			   stbc ? "NSTS" : "NSS", nsts, rate);
+
+	/* bandwidth */
+	seq_printf(s, "%s, ",
+		   frmode < 5 ? mt7925_tx_rate_bw_str[frmode] :
+		   mt7925_tx_rate_bw_str[5]);
+
+	/* GI / preamble */
+	if (txmode == MT7925_TX_RATE_MODE_CCK)
+		seq_printf(s, "%s, ", rate < 4 ? "LP" : "SP");
+	else if (txmode == MT7925_TX_RATE_MODE_OFDM)
+		; /* OFDM has no GI/preamble to print */
+	else if (txmode == MT7925_TX_RATE_MODE_HTMIX ||
+		 txmode == MT7925_TX_RATE_MODE_HTGF ||
+		 txmode == MT7925_TX_RATE_MODE_VHT ||
+		 txmode == MT7925_TX_RATE_MODE_PLR)
+		seq_printf(s, "%s, ", sgi == 0 ? "LGI" : "SGI");
+	else
+		seq_printf(s, "%s, ",
+			   sgi == 0 ? "SGI" : (sgi == 1 ? "MGI" : "LGI"));
+
+	/* mode, DCM, 106t, STBC, coding, SPE */
+	seq_printf(s, "%s%s%s%s%s%s%d\n",
+		   mode_str,
+		   dcm ? ", DCM" : "", ersu106t ? ", 106t" : "",
+		   stbc ? ", STBC, " : ", ", ldpc == 0 ? "BCC" : "LDPC",
+		   ", SPE", spe);
+
+	/* chip out TX power (unit: 0.5 dBm) */
+	pos_txpwr = (txpwr < 0) ? (~txpwr + 1) : txpwr;
+	seq_printf(s, "%-22s  %s = %c%d.%1d dBm\n",
+		   " ", "Chip Out TX Power",
+		   (txpwr < 0) ? '-' : '+',
+		   (pos_txpwr / 2), 5 * (pos_txpwr % 2));
+}
+
+/* Guard interval used for the auto-rate entries, mirror Gen4m
+ * connac3x_get_sgi_info_from_fw(): pick the per-mode/per-BW flag.
+ */
+u8
+mt7925_wtbl_get_sgi(const struct mt7925_wtbl_rate *wr, u8 txmode, u8 fcap)
+{
+	bool he = (txmode == MT7925_TX_RATE_MODE_HE_SU ||
+		   txmode == MT7925_TX_RATE_MODE_HE_ER ||
+		   txmode == MT7925_TX_RATE_MODE_HE_TRIG ||
+		   txmode == MT7925_TX_RATE_MODE_HE_MU);
+	bool eht = (txmode == MT7925_TX_RATE_MODE_EHT_ER ||
+		    txmode == MT7925_TX_RATE_MODE_EHT_TRIG ||
+		    txmode == MT7925_TX_RATE_MODE_EHT_MU);
+
+	if (eht)
+		return wr->gi_eht;
+
+	switch (fcap) {
+	case MT7925_BW_20:
+		return he ? wr->g2_he : wr->g2;
+	case MT7925_BW_40:
+		return he ? wr->g4_he : wr->g4;
+	case MT7925_BW_80:
+		return he ? wr->g8_he : wr->g8;
+	case MT7925_BW_160:
+		return he ? wr->g16_he : wr->g16;
+	default:
+		return 0;
+	}
+}
+
+/* Coding used for the auto-rate entries, mirror Gen4m
+ * connac3x_wtbl_get_ldpc_info_from_fw(): 0 = BCC, 1 = LDPC.
+ */
+u8
+mt7925_wtbl_get_ldpc(const struct mt7925_wtbl_rate *wr, u8 txmode)
+{
+	switch (txmode) {
+	case MT7925_TX_RATE_MODE_HTMIX:
+	case MT7925_TX_RATE_MODE_HTGF:
+		return wr->ht_ldpc;
+	case MT7925_TX_RATE_MODE_VHT:
+		return wr->vht_ldpc;
+	case MT7925_TX_RATE_MODE_HE_SU:
+	case MT7925_TX_RATE_MODE_HE_ER:
+	case MT7925_TX_RATE_MODE_HE_TRIG:
+	case MT7925_TX_RATE_MODE_HE_MU:
+		return wr->he_ldpc;
+	case MT7925_TX_RATE_MODE_EHT_ER:
+	case MT7925_TX_RATE_MODE_EHT_TRIG:
+	case MT7925_TX_RATE_MODE_EHT_MU:
+		return wr->eht_ldpc;
+	default:
+		return 0;
+	}
+}
+
+int mt792x_mcu_get_stat(struct mt792x_dev *dev,
+			struct mt7925_sta_stats *stats)
+{
+	u16 len = 0;
+	int ret = 0;
+	struct mt7925_sta_stats_event *event = NULL;
+	struct sk_buff *skb = NULL;
+	struct mt7925_sta_stats_req req = {
+		.tag = cpu_to_le16(UNI_CMD_ID_STATISTICS_STA),
+		.len = cpu_to_le16(sizeof(req) - sizeof(req.rsv)),
+		.lls_read_clear = false,
+		.reset_counter = false,
+	};
+
+	ret = mt7925_get_ap_sta_rec_idx(dev, &req.sta_rec_idx);
+	if (ret)
+		return ret;
+
+	if (!stats)
+		return -EINVAL;
+
+	ret = mt76_mcu_send_and_get_msg(&dev->mt76,
+					MCU_UNI_QUERY(GET_STAT_INFO),
+					&req, sizeof(req), true, &skb);
+
+	if (ret)
+		return ret;
+
+	event = (struct mt7925_sta_stats_event *)skb->data;
+
+	if (le16_to_cpu(event->tag) != UNI_CMD_ID_STATISTICS_STA) {
+		ret = -EPROTO;
+		goto out;
+	}
+
+	len = le16_to_cpu(event->len);
+	if (len < sizeof(*event) - sizeof(event->rsv) ||
+	    len > skb->len - sizeof(event->rsv) || !IS_ALIGNED(len, 4)) {
+		ret = -EMSGSIZE;
+		goto out;
+	}
+
+	if (event->stats.version != MT7925_STA_STATS_VERSION) {
+		ret = -EPROTO;
+		goto out;
+	}
+
+	if (!(le32_to_cpu(event->stats.flags) & MT7925_STA_STATS_VALID)) {
+		ret = -ENOENT;
+		goto out;
+	}
+
+	memcpy(stats, &event->stats, sizeof(*stats));
+
+out:
+	dev_kfree_skb(skb);
+	return ret;
+}
+
+int mt7925_mcu_get_mib_info(struct mt792x_dev *dev, u8 band_idx,
+			    const u32 *counters, u8 counter_num,
+			    u64 *values)
+{
+	u8 *req;
+	u8 *tlv, *tlv_end;
+	u16 req_len;
+	int i, ret;
+	struct mt7925_mib_hdr *req_hdr;
+	struct mt7925_mib_data *req_tlv;
+	struct sk_buff *skb;
+
+	if (!counters || !values || !counter_num ||
+	    counter_num > MT7925_MIB_MAX_CNT)
+		return -EINVAL;
+
+	/* clear output; counters not returned by firmware stay 0 */
+	memset(values, 0, counter_num * sizeof(*values));
+
+	req_len = sizeof(struct mt7925_mib_hdr) +
+		  counter_num * sizeof(struct mt7925_mib_data);
+
+	req = kzalloc(req_len, GFP_KERNEL);
+	if (!req)
+		return -ENOMEM;
+
+	req_hdr = (struct mt7925_mib_hdr *)req;
+	req_hdr->band_idx = band_idx;
+
+	req_tlv = (struct mt7925_mib_data *)(req + sizeof(*req_hdr));
+	for (i = 0; i < counter_num; i++) {
+		req_tlv[i].tag = cpu_to_le16(UNI_CMD_MIB_DATA);
+		req_tlv[i].len = cpu_to_le16(sizeof(struct mt7925_mib_data));
+		req_tlv[i].counter = cpu_to_le32(counters[i]);
+	}
+
+	ret = mt76_mcu_send_and_get_msg(&dev->mt76,
+					MCU_UNI_QUERY(GET_MIB_INFO), req,
+					req_len, true, &skb);
+	kfree(req);
+	if (ret)
+		return ret;
+
+	if (skb->len < sizeof(struct mt7925_mib_hdr)) {
+		ret = -EMSGSIZE;
+		goto out;
+	}
+
+	/* walk the returned TLVs, match each counter ID to the request */
+	tlv = skb->data + sizeof(struct mt7925_mib_hdr);
+	tlv_end = skb->data + skb->len;
+
+	while (tlv + sizeof(struct mt7925_mib_data) <= tlv_end) {
+		struct mt7925_mib_data *evt_tlv =
+			(struct mt7925_mib_data *)tlv;
+		u16 tag = le16_to_cpu(evt_tlv->tag);
+		u16 len = le16_to_cpu(evt_tlv->len);
+		u32 counter;
+
+		if (len < sizeof(struct mt7925_mib_data) ||
+		    tlv + len > tlv_end)
+			break;
+
+		if (tag == UNI_CMD_MIB_DATA) {
+			counter = le32_to_cpu(evt_tlv->counter);
+
+			if (counter < MT7925_MIB_CNT_MAX_NUM) {
+				for (i = 0; i < counter_num; i++) {
+					if (counters[i] == counter) {
+						values[i] =
+						  le64_to_cpu(evt_tlv->data);
+						break;
+					}
+				}
+			}
+		}
+
+		tlv += len;
+	}
+
+	ret = 0;
+
+out:
+	dev_kfree_skb(skb);
+
+	return ret;
+}
+
+char *mt7925_hw_rate_ofdm_str(u8 ofdm_idx)
+{
+	char * const ofdm_str[] = {
+		"6M", "9M", "12M", "18M", "24M", "36M", "48M", "54M", "N/A"
+	};
+
+	switch (ofdm_idx) {
+	case 11: return ofdm_str[0];	/* 6M */
+	case 15: return ofdm_str[1];	/* 9M */
+	case 10: return ofdm_str[2];	/* 12M */
+	case 14: return ofdm_str[3];	/* 18M */
+	case 9:  return ofdm_str[4];	/* 24M */
+	case 13: return ofdm_str[5];	/* 36M */
+	case 8:  return ofdm_str[6];	/* 48M */
+	case 12: return ofdm_str[7];	/* 54M */
+	default: return ofdm_str[8];
+	}
+}
+
+/* mt792x_mcu_get_wtbl_rate - fetch the firmware auto-rate table for a WCID
+ * @dev:	device
+ * @wlan_idx:	HW WTBL / WCID index (see mt7925_sta_stats.wtbl_idx)
+ * @rate:	out, filled with the auto-rate table snapshot
+ *
+ * Mirror of Gen4m wlanoidGetStatWtbl(): send UNI_CMD_GET_STAT_INFO with the
+ * WTBL rate TLV (cmd tag 10) and parse the reply (event tag 12) into @rate.
+ *
+ * Return: 0 on success, negative errno otherwise.
+ */
+int mt792x_mcu_get_wtbl_rate(struct mt792x_dev *dev, u16 wlan_idx,
+			     struct mt7925_wtbl_rate *rate)
+{
+	int ret;
+	struct mt7925_wtbl_rate_event *event = NULL;
+	struct sk_buff *skb = NULL;
+	struct mt7925_wtbl_rate_req req = {
+		.tag = cpu_to_le16(UNI_CMD_ID_STATISTICS_WTBL),
+		.len = cpu_to_le16(sizeof(req) - sizeof(req.rsv)),
+		.wlan_idx = cpu_to_le16(wlan_idx),
+	};
+
+	if (!rate)
+		return -EINVAL;
+
+	ret = mt76_mcu_send_and_get_msg(&dev->mt76,
+					MCU_UNI_QUERY(GET_STAT_INFO),
+					&req, sizeof(req), true, &skb);
+	if (ret)
+		return ret;
+
+	if (skb->len < sizeof(*event)) {
+		ret = -EMSGSIZE;
+		goto out;
+	}
+
+	event = (struct mt7925_wtbl_rate_event *)skb->data;
+
+	if (le16_to_cpu(event->rate.tag) != MT7925_UNI_EVENT_STATISTICS_WTBL) {
+		ret = -EPROTO;
+		goto out;
+	}
+
+	memcpy(rate, &event->rate, sizeof(*rate));
+	ret = 0;
+
+out:
+	dev_kfree_skb(skb);
+	return ret;
+}
+
+/**
+ * mt7925_mcu_get_link_quality - query RSSI and TX link speed for a band
+ * @dev:	device
+ * @band_idx:	band/BSS index (0..3)
+ * @rssi:	out, moving-average beacon RSSI in dBm (may be NULL)
+ * @tx_link_speed_kbps: out, TX rate in kbit/s (may be NULL)
+ *
+ * Return: 0 on success, -ENOENT if the band's link quality is not ready.
+ *
+ * Only RSSI and TX rate come from firmware here; for RX rate read wcid->rate.
+ */
+int mt7925_mcu_get_link_quality(struct mt792x_dev *dev, u8 band_idx,
+				s8 *rssi, u32 *tx_link_speed_kbps)
+{
+	int ret;
+	struct mt7925_lq_event *event;
+	struct mt7925_link_quality *lq;
+	struct sk_buff *skb;
+	struct mt7925_lq_req req = {
+		.tag = cpu_to_le16(UNI_CMD_ID_STATISTICS_LINK_QUALITY),
+		.len = cpu_to_le16(sizeof(req) - sizeof(req.rsv)),
+	};
+
+	if (band_idx >= MT7925_LQ_BAND_NUM)
+		return -EINVAL;
+
+	ret = mt76_mcu_send_and_get_msg(&dev->mt76,
+					MCU_UNI_QUERY(GET_STAT_INFO), &req,
+					sizeof(req), true, &skb);
+	if (ret)
+		return ret;
+
+	if (skb->len < sizeof(*event)) {
+		ret = -EMSGSIZE;
+		goto out;
+	}
+
+	event = (struct mt7925_lq_event *)skb->data;
+
+	if (le16_to_cpu(event->tag) != UNI_CMD_ID_STATISTICS_LINK_QUALITY) {
+		ret = -EPROTO;
+		goto out;
+	}
+
+	lq = &event->lq[band_idx];
+	if (!lq->is_ready) {
+		ret = -ENOENT;
+		goto out;
+	}
+
+	if (rssi)
+		*rssi = lq->rssi;
+
+	/* firmware reports TX rate in 500 Kb/s units -> kbit/s */
+	if (tx_link_speed_kbps)
+		*tx_link_speed_kbps = le16_to_cpu(lq->link_speed) * 500;
+
+	ret = 0;
+
+out:
+	dev_kfree_skb(skb);
+
+	return ret;
+}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/stats.h b/drivers/net/wireless/mediatek/mt76/mt7925/stats.h
new file mode 100644
index 000000000000..2e29b20cf27b
--- /dev/null
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/stats.h
@@ -0,0 +1,305 @@
+/* SPDX-License-Identifier: BSD-3-Clause-Clear */
+/* Copyright (C) 2026 MediaTek Inc. */
+
+#ifndef __MT7925_STATS_H
+#define __MT7925_STATS_H
+
+#include <linux/if_ether.h>
+#include <linux/types.h>
+#include "../mt76_connac_mcu.h"
+
+#define MT7925_STA_STATS_AC_NUM 4
+#define MT7925_STA_STATS_BAND_NUM 2
+#define MT7925_STA_STATS_AGG_RANGE_NUM 16
+#define MT7925_STA_STATS_TX_QUALITY_NUM 4
+#define MT7925_TX_MODE_NUM      16
+#define MT7925_TX_RATE_CCK_NUM  5
+#define MT7925_TX_RATE_BW_NUM   6
+
+/* HW rate code (16-bit) field extract, mirror Gen4m CONNAC3X_HW_TX_RATE_TO_*.
+ * The auto-rate table stores one rate code per entry.
+ */
+#define MT7925_HW_RATE_TO_MCS(_x)	(((_x) & GENMASK(5, 0)) >> 0)
+#define MT7925_HW_RATE_TO_MODE(_x)	(((_x) & GENMASK(9, 6)) >> 6)
+#define MT7925_HW_RATE_TO_NSS(_x)	(((_x) & GENMASK(13, 10)) >> 10)
+#define MT7925_HW_RATE_TO_STBC(_x)	(((_x) & BIT(14)) >> 14)
+#define MT7925_HW_RATE_TO_DCM(_x)	(((_x) & BIT(4)) >> 4)
+#define MT7925_HW_RATE_TO_106T(_x)	(((_x) & BIT(5)) >> 5)
+#define MT7925_HW_RATE_UNMASK_DCM(_r)	((u8)(_r) & 0xef)
+#define MT7925_HW_RATE_UNMASK_106T(_r)	((u8)(_r) & 0xdf)
+
+/* Number of entries in the firmware auto-rate table (mirror Gen4m
+ * AUTO_RATE_NUM). RA cycles through these and reports which one is in use
+ * via wtbl->rate_idx.
+ */
+#define MT7925_AUTO_RATE_NUM 8
+
+/* Firmware EVENT_STATISTICS sub-event tags. The WTBL rate request uses cmd tag
+ * UNI_CMD_ID_STATISTICS_WTBL (10) but the firmware replies with event tag 12.
+ */
+#define MT7925_UNI_EVENT_STATISTICS_WTBL 12
+
+/* CONNAC3X TX Vector BBP latch bit fields (mirror cmm_asic_connac3x.h).
+ * These operate on the 3-DWORD tx_vector[band].txv[3] array.
+ */
+#define MT7925_TXV_GET_TX_RATE(_txv)	((_txv)[2] & 0x7f)
+#define MT7925_TXV_GET_TX_LDPC(_txv)	(((_txv)[2] & (0x1 << 7)) >> 7)
+#define MT7925_TXV_GET_TX_STBC(_txv)	(((_txv)[0] & (0x3 << 6)) >> 6)
+#define MT7925_TXV_GET_TX_FRMODE(_txv)	(((_txv)[0] & (0x7 << 8)) >> 8)
+#define MT7925_TXV_GET_TX_MODE(_txv)	(((_txv)[0] & (0xf << 12)) >> 12)
+#define MT7925_TXV_GET_TX_NSTS(_txv)	(((_txv)[2] & (0xfU << 28)) >> 28)
+#define MT7925_TXV_GET_TX_PWR(_txv)	(((_txv)[0] & (0xff << 16)) >> 16)
+#define MT7925_TXV_GET_TX_SGI(_txv)	(((_txv)[1] & (0x3 << 26)) >> 26)
+#define MT7925_TXV_GET_TX_SPE_IDX(_txv)	(((_txv)[0] & (0x1f << 0)) >> 0)
+#define MT7925_TXV_GET_TX_DCM(_txv)	(((_txv)[2] & (0x1 << 4)) >> 4)
+#define MT7925_TXV_GET_TX_106T(_txv)	(((_txv)[2] & (0x1 << 5)) >> 5)
+#define MT7925_TXV_RATE_UNMASK_DCM(_r)	((u8)(_r) & 0xef)
+#define MT7925_TXV_RATE_UNMASK_106T(_r)	((u8)(_r) & 0xdf)
+
+extern const char * const mt7925_tx_mode_str[MT7925_TX_MODE_NUM];
+extern const char * const mt7925_tx_rate_cck_str[MT7925_TX_RATE_CCK_NUM];
+extern const char * const mt7925_tx_rate_bw_str[MT7925_TX_RATE_BW_NUM];
+
+/* TX rate mode, mirror Gen4m TX_RATE_MODE_* (see nic_rate.h). Used to decode
+ * both the TX Vector BBP latch and the firmware auto-rate table rate codes.
+ */
+enum mt7925_tx_rate_mode {
+	MT7925_TX_RATE_MODE_CCK = 0,
+	MT7925_TX_RATE_MODE_OFDM = 1,
+	MT7925_TX_RATE_MODE_HTMIX = 2,
+	MT7925_TX_RATE_MODE_HTGF = 3,
+	MT7925_TX_RATE_MODE_VHT = 4,
+	MT7925_TX_RATE_MODE_PLR = 5,
+	MT7925_TX_RATE_MODE_HE_SU = 8,
+	MT7925_TX_RATE_MODE_HE_ER = 9,
+	MT7925_TX_RATE_MODE_HE_TRIG = 10,
+	MT7925_TX_RATE_MODE_HE_MU = 11,
+	MT7925_TX_RATE_MODE_EHT_ER = 13,
+	MT7925_TX_RATE_MODE_EHT_TRIG = 14,
+	MT7925_TX_RATE_MODE_EHT_MU = 15,
+};
+
+enum stats_query_mib_array_index {
+	QUERY_MIB_CNT_RX_FCS_ERR = 0,
+	QUERY_MIB_CNT_RX_FIFO_OVERFLOW,
+	QUERY_MIB_CNT_RX_MPDU,
+	QUERY_MIB_CNT_AMPDU_RX_COUNT,
+	QUERY_MIB_CNT_PF_DROP,
+	QUERY_MIB_CNT_LEN_MISMATCH,
+	QUERY_MIB_CNT_CHANNEL_IDLE,
+	QUERY_MIB_CNT_CCA_NAV_TX_TIME,
+	QUERY_MIB_CNT_MDRDY,
+	QUERY_MIB_CNT_RX_CCK_MDRDY_TIME,
+	QUERY_MIB_CNT_RX_OFDM_LG_MIXED_MDRDY_TIME,
+	QUERY_MIB_CNT_RX_OFDM_GREEN_MDRDY_TIME,
+	QUERY_MIB_CNT_P_CCA_TIME,
+	QUERY_MIB_CNT_S_CCA_TIME,
+	QUERY_MIB_CNT_P_ED_TIME,
+	QUERY_MIB_CNT_BCN_TX,
+	QUERY_MIB_CNT_TX_BW_40MHZ,
+	QUERY_MIB_CNT_TX_BW_80MHZ,
+	QUERY_MIB_CNT_TX_BW_160MHZ,
+	QUERY_MIB_CNT_BSS0_BA_MISS,
+	QUERY_MIB_CNT_BSS0_RTS_TX_CNT,
+	QUERY_MIB_CNT_BSS0_FRAME_RETRY,
+	QUERY_MIB_CNT_BSS0_FRAME_RETRY_2,
+	QUERY_MIB_CNT_BSS0_RTS_RETRY,
+	QUERY_MIB_CNT_BSS0_ACK_FAIL,
+	QUERY_MIB_CNT_AMPDU,
+	QUERY_MIB_CNT_AMPDU_MPDU,
+	QUERY_MIB_CNT_AMPDU_ACKED,
+	QUERY_MIB_CNT_MAX_NUM,
+};
+
+enum stats_trx_agg_range {
+	STAT_MIB_CNT_TRX_AGG_RANGE0_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE1_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE2_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE3_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE4_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE5_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE6_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE7_IDX,
+	STAT_MIB_CNT_TRX_AGG_RANGE_MAX_NUM
+};
+
+enum {
+	MT7925_BW_20 = 0,
+	MT7925_BW_40,
+	MT7925_BW_80,
+	MT7925_BW_160,
+};
+
+struct mt7925_sta_stats_ac {
+	__le32 tx_fail;
+	__le32 tx_retry;
+} __packed;
+
+struct mt7925_sta_stats_tx_vector {
+	__le32 txv[3];
+} __packed;
+
+struct mt7925_sta_stats_mib {
+	__le32 rx_mpdu_cnt;
+	__le32 fcs_error;
+	__le32 rx_fifo_full;
+	__le32 ampdu_tx_sf_cnt;
+	__le32 ampdu_tx_ack_sf_cnt;
+	__le32 tx_range_ampdu_cnt[MT7925_STA_STATS_AGG_RANGE_NUM];
+} __packed;
+
+/* Firmware EVENT_STA_STATISTICS body. */
+struct mt7925_sta_stats {
+	u8 version;
+	u8 rsv[3];
+	__le32 flags;
+
+	u8 sta_idx;
+	u8 bss_idx;
+	u8 wtbl_idx;
+	u8 rsv1;
+
+	u8 mac_addr[ETH_ALEN];
+	u8 per;
+	u8 rcpi;
+
+	__le32 phy_mode;
+	__le16 link_speed; /* 0.5 Mbit/s units */
+	u8 link_quality;
+	u8 link_reserved;
+
+	__le32 tx_count;
+	__le32 tx_fail_count;
+	__le32 tx_life_timeout_count;
+	__le32 tx_done_air_time;
+	__le32 transmit_count;
+	__le32 transmit_fail_count;
+
+	struct mt7925_sta_stats_ac ac[MT7925_STA_STATS_AC_NUM];
+
+	u8 temperature;
+	u8 skip_ar;
+	u8 ar_table_idx;
+	u8 rate_entry_idx;
+	u8 rate_entry_idx_prev;
+	u8 tx_sgi_detect_pass_cnt;
+	u8 ave_per;
+	u8 rsv2;
+
+	__le32 agg_range_ctrl[2];
+	u8 range_mode;
+	u8 rsv3[3];
+	__le32 agg_range_ctrl_ext[6];
+
+	u8 ar_state_curr;
+	u8 ar_state_prev;
+	u8 ar_action_type;
+	u8 highest_rate_cnt;
+	u8 lowest_rate_cnt;
+	u8 rsv4;
+	__le16 train_up;
+	__le16 train_down;
+	u8 rsv5[2];
+	__le32 rate1_tx_cnt;
+	__le32 rate1_fail_cnt;
+
+	struct mt7925_sta_stats_tx_vector tx_vector[MT7925_STA_STATS_BAND_NUM];
+	struct mt7925_sta_stats_mib mib[MT7925_STA_STATS_BAND_NUM];
+
+	u8 is_force_tx_stream;
+	u8 is_force_se_off;
+	__le16 ra_running_cnt;
+	u8 ra_status;
+	u8 max_ampdu_factor;
+	u8 tx_quality[MT7925_STA_STATS_TX_QUALITY_NUM];
+	u8 tx_rate_up_penalty;
+	u8 low_traffic_mode;
+	u8 low_traffic_count;
+	u8 low_traffic_dashboard;
+	u8 dynamic_sgi_state;
+	u8 dynamic_sgi_score;
+	u8 dynamic_bw_state;
+	u8 dynamic_gband_256qam_state;
+	u8 vht_non_sp_rate_state;
+	u8 rsv6[3];
+	u8 rsv7[2];
+} __packed;
+
+struct mt7925_sta_rec_lookup {
+	int sta_rec_idx;
+};
+
+/* Firmware auto-rate table snapshot, mirror Gen4m struct UNI_EVENT_STAT_WTBL.
+ *
+ * The firmware RA maintains up to MT7925_AUTO_RATE_NUM candidate rates in
+ * rate_code[]. rate_idx tells which entry is currently in use, so a dump can
+ * point at it with "-->". fcap is the bandwidth capability and the ldpc/sgi
+ * flags describe the per-mode coding/guard-interval used for those rates.
+ */
+struct mt7925_wtbl_rate {
+	__le16 tag;
+	__le16 len;
+	u8 rx_rcpi0;
+	u8 rx_rcpi1;
+	u8 cbrn;
+	u8 fcap;
+	u8 spe_idx;
+	u8 g2;			/* SGI enabled, non-HE, BW20 */
+	u8 g4;			/* SGI enabled, non-HE, BW40 */
+	u8 g8;			/* SGI enabled, non-HE, BW80 */
+	u8 g16;			/* SGI enabled, non-HE, BW160 */
+	u8 g2_he;		/* GI, HE, BW20 */
+	u8 g4_he;		/* GI, HE, BW40 */
+	u8 g8_he;		/* GI, HE, BW80 */
+	u8 g16_he;		/* GI, HE, BW160 */
+	u8 gi_eht;		/* GI, EHT */
+	u8 ht_ldpc;
+	u8 vht_ldpc;
+	u8 he_ldpc;
+	u8 eht_ldpc;
+	u8 af;
+	u8 rate_idx;		/* index into rate_code[] currently in use */
+	__le16 rate_code[MT7925_AUTO_RATE_NUM];
+	__le32 rsv[4];
+} __packed;
+
+int
+mt7925_get_ap_sta_rec_idx(struct mt792x_dev *dev, u8 *sta_rec_idx);
+
+void
+mt7925_print_last_tx_rate(struct seq_file *s, const __le32 le_txv[3]);
+
+u8
+mt7925_wtbl_get_sgi(const struct mt7925_wtbl_rate *wr, u8 txmode, u8 fcap);
+
+u8
+mt7925_wtbl_get_ldpc(const struct mt7925_wtbl_rate *wr, u8 txmode);
+
+char *mt7925_hw_rate_ofdm_str(u8 ofdm_idx);
+
+int mt792x_mcu_get_wtbl_rate(struct mt792x_dev *dev, u16 wlan_idx,
+			     struct mt7925_wtbl_rate *rate);
+
+int mt792x_mcu_get_stat(struct mt792x_dev *dev,
+			struct mt7925_sta_stats *stats);
+
+int mt7925_mcu_get_mib_info(struct mt792x_dev *dev, u8 band_idx,
+			    const u32 *counters, u8 counter_num,
+			    u64 *values);
+
+/* GET_STAT_INFO (UNI_CMD_ID_GET_STATISTICS = 0x23),
+ * sub-command UNI_CMD_GET_STATISTICS_TAG_LINK_QUALITY = 1.
+ *
+ * @band_idx:            band/BSS index (0..3)
+ * @rssi:                out, moving-average beacon RSSI (dBm), may be NULL
+ * @tx_link_speed_kbps:  out, TX link speed in kbit/s, may be NULL
+ *
+ * Returns 0 on success, -ENOENT if the band's link quality is not ready.
+ *
+ * NOTE: firmware provides only RSSI and TX rate here. RX rate/bandwidth are
+ * not in this event; read wcid->rate for the RX rate.
+ */
+int mt7925_mcu_get_link_quality(struct mt792x_dev *dev, u8 band_idx,
+				s8 *rssi, u32 *tx_link_speed_kbps);
+
+#endif
-- 
2.45.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.