[RFC PATCH wireless] wifi: mt76: mt7925: get the survey airtime from the firmware

Chris hoopyfrood42 at gmail.com
Fri Oct 23 15:34:01 PDT 2026


On Fri, Oct 23, 2026 at 2:06 PM Devin Wittmayer
<lucid_duck at justthetip.ca> wrote:
>
> Busy time in the mt7925 survey is bogus. The register it comes from
> doesn't work on this chip, so every update adds the same large number
> and busy time soon outgrows the time spent on the channel. Transmit
> and receive time come out far too low as well.
>
> Ask the firmware for all three instead, the way mt7996 does.
>
> The MT7927 counts 5 and 6 GHz on a second band that the survey never
> read, so read both bands there and only add the one the channel is on.
>
> Fixes: c948b5da6bbe ("wifi: mt76: mt7925: add Mediatek Wi-Fi7 driver for mt7925 chips")
> Fixes: 05fd14885303 ("wifi: mt76: mt7925: sync MT7927 BSS band assignment")
> Reported-by: Chris Kelly <hoopyfrood42 at gmail.com>
> Closes: https://lore.kernel.org/linux-wireless/CAHaeJsT8O8Nhq351hFp3NAL8e0iU85N-LBdYZ9nYHFHVy7t_hQ@mail.gmail.com/
> Signed-off-by: Devin Wittmayer <lucid_duck at justthetip.ca>
> ---
> Chris, here's the RFC you asked about. Busy time now follows the load
> and stays under the time on the channel, hopping every half second too.
>
> It's an RFC because the offsets aren't documented. On an A9000:
>
>   19  busy time, including the card's own transmit
>   20  receive time
>   28  transmit time
>
> The MT7927 counts 5 and 6 GHz on a second band, which keeps counting
> while unused, so only the band in use is added.
>
> Busy as a share of time on the channel, A9000 on 2.4 GHz next to an
> MT7921AU:
>
>                before    after    MT7921AU
>   idle         4229%     4.2%     1.4%
>   100 Mbit/s   4317%     37.3%    33.7%
>   saturated    4346%     79.5%    77.4%
>
> Tested on the A9000 (Arch 6.18 and 7.2, Ubuntu 7.3, wireless-next with
> lockdep) and on a PCIe MT7925 and MT7927 (Fedora 7.2). mt7921 keeps its
> registers, which work on my MT7921AU.
>
> Could you try it on your A9000 against the Wi-Spy?
>
>  .../net/wireless/mediatek/mt76/mt7925/mac.c   | 42 +++++++++++
>  .../net/wireless/mediatek/mt76/mt7925/main.c  |  1 +
>  .../net/wireless/mediatek/mt76/mt7925/mcu.c   | 70 +++++++++++++++++++
>  .../net/wireless/mediatek/mt76/mt7925/mcu.h   | 17 +++++
>  .../wireless/mediatek/mt76/mt7925/mt7925.h    |  3 +
>  .../net/wireless/mediatek/mt76/mt7925/pci.c   |  2 +-
>  .../net/wireless/mediatek/mt76/mt7925/usb.c   |  2 +-
>  drivers/net/wireless/mediatek/mt76/mt792x.h   |  1 +
>  8 files changed, 136 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mac.c b/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
> index 7af996abe294..4ec3712fe614 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
> @@ -1470,6 +1470,48 @@ void mt7925_queue_rx_skb(struct mt76_dev *mdev, enum mt76_rxq_id q,
>  }
>  EXPORT_SYMBOL_GPL(mt7925_queue_rx_skb);
>
> +void mt7925_mac_update_chan_time(struct mt792x_phy *phy, bool reset)
> +{
> +       struct ieee80211_channel *chan = phy->mt76->chandef.chan;
> +       struct mt76_channel_state *state = phy->mt76->chan_state;
> +       struct mt792x_dev *dev = phy->dev;
> +       int i, n_bands = 1, band = 0;
> +
> +       /* The MT7927 counts 2.4 GHz on band 0 and 5/6 GHz on band 1, and
> +        * the band not in use keeps counting, so read both every time and
> +        * only add the band the channel is on.
> +        */
> +       if (is_mt7927(&dev->mt76)) {
> +               n_bands = ARRAY_SIZE(phy->state_ts);
> +               if (chan)
> +                       band = mt7927_band_idx(chan->band);
> +       }
> +
> +       for (i = 0; i < n_bands; i++) {
> +               bool add = !reset && i == band;
> +               u32 bss_rx;
> +
> +               bss_rx = mt76_get_field(dev, MT_MIB_SDR37(i),
> +                                       MT_MIB_SDR37_RXTIME_MASK);
> +               if (add)
> +                       state->cc_bss_rx += bss_rx;
> +
> +               mt7925_mcu_get_chan_mib_info(phy, i, add);
> +       }
> +}
> +
> +void mt7925_update_channel(struct mt76_phy *mphy)
> +{
> +       struct mt792x_dev *dev = container_of(mphy->dev, struct mt792x_dev, mt76);
> +
> +       if (mt76_connac_pm_wake(mphy, &dev->pm))
> +               return;
> +
> +       mt7925_mac_update_chan_time(mphy->priv, false);
> +       mt76_connac_power_save_sched(mphy, &dev->pm);
> +}
> +EXPORT_SYMBOL_GPL(mt7925_update_channel);
> +
>  static void
>  mt7925_vif_connect_iter(void *priv, u8 *mac,
>                         struct ieee80211_vif *vif)
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/main.c b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
> index 22cc5b49da0a..393c225c19e4 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/main.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
> @@ -351,6 +351,7 @@ int __mt7925_start(struct mt792x_phy *phy)
>                 return err;
>
>         mt792x_mac_reset_counters(phy);
> +       mt7925_mac_update_chan_time(phy, true);
>         set_bit(MT76_STATE_RUNNING, &mphy->state);
>
>         ieee80211_queue_delayed_work(mphy->hw, &mphy->mac_work,
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
> index f299a3379adb..b741a4880cfe 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
> @@ -1057,6 +1057,76 @@ int mt7925_mcu_get_temperature(struct mt792x_phy *phy)
>         return temperature;
>  }
>
> +int mt7925_mcu_get_chan_mib_info(struct mt792x_phy *phy, u8 band, bool add)
> +{
> +       enum {
> +               IDX_BUSY_TIME,
> +               IDX_TX_TIME,
> +               IDX_RX_TIME,
> +               IDX_NUM
> +       };
> +       struct {
> +               struct {
> +                       u8 band;
> +                       u8 __rsv[3];
> +               } hdr;
> +               struct {
> +                       __le16 tag;
> +                       __le16 len;
> +                       __le32 offs;
> +               } data[IDX_NUM];
> +       } __packed req = {
> +               .hdr.band = band,
> +       };
> +       static const u32 offs[] = {
> +               [IDX_BUSY_TIME] = UNI_MIB_BUSY_TIME,
> +               [IDX_TX_TIME] = UNI_MIB_TX_TIME,
> +               [IDX_RX_TIME] = UNI_MIB_RX_TIME,
> +       };
> +       struct mt76_channel_state *state = phy->mt76->chan_state;
> +       struct mt76_channel_state *state_ts = &phy->state_ts[band];
> +       struct mt792x_dev *dev = phy->dev;
> +       struct mt7925_mcu_mib *res;
> +       struct sk_buff *skb;
> +       int i, ret;
> +
> +       for (i = 0; i < IDX_NUM; i++) {
> +               req.data[i].tag = cpu_to_le16(UNI_CMD_MIB_DATA);
> +               req.data[i].len = cpu_to_le16(sizeof(req.data[i]));
> +               req.data[i].offs = cpu_to_le32(offs[i]);
> +       }
> +
> +       ret = mt76_mcu_send_and_get_msg(&dev->mt76,
> +                                       MCU_WM_UNI_CMD_QUERY(GET_MIB_INFO),
> +                                       &req, sizeof(req), true, &skb);
> +       if (ret)
> +               return ret;
> +       if (skb->len < sizeof(req.hdr) + IDX_NUM * sizeof(*res)) {
> +               dev_kfree_skb(skb);
> +               return -EINVAL;
> +       }
> +
> +       skb_pull(skb, sizeof(req.hdr));
> +       res = (struct mt7925_mcu_mib *)skb->data;
> +
> +       if (!add)
> +               goto out;
> +
> +#define __res_u64(s) le64_to_cpu(res[s].data)
> +       state->cc_busy += __res_u64(IDX_BUSY_TIME) - state_ts->cc_busy;
> +       state->cc_tx += __res_u64(IDX_TX_TIME) - state_ts->cc_tx;
> +       state->cc_rx += __res_u64(IDX_RX_TIME) - state_ts->cc_rx;
> +out:
> +       state_ts->cc_busy = __res_u64(IDX_BUSY_TIME);
> +       state_ts->cc_tx = __res_u64(IDX_TX_TIME);
> +       state_ts->cc_rx = __res_u64(IDX_RX_TIME);
> +#undef __res_u64
> +
> +       dev_kfree_skb(skb);
> +
> +       return 0;
> +}
> +
>  static void
>  mt7925_mcu_parse_phy_cap(struct mt792x_dev *dev, char *data)
>  {
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
> index 11f9eac13ffc..f3e892dbb74c 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.h
> @@ -57,6 +57,23 @@ struct mt7925_mcu_reg_event {
>         __le32 val;
>  } __packed;
>
> +enum {
> +       UNI_CMD_MIB_DATA,
> +};
> +
> +enum {
> +       UNI_MIB_BUSY_TIME = 19,
> +       UNI_MIB_RX_TIME = 20,
> +       UNI_MIB_TX_TIME = 28,
> +};
> +
> +struct mt7925_mcu_mib {
> +       __le16 tag;
> +       __le16 len;
> +       __le32 offs;
> +       __le64 data;
> +} __packed;
> +
>  struct mt7925_mcu_ant_id_config {
>         u8 ant_id[4];
>  } __packed;
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h b/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
> index 636efe038392..706e67e8ec42 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/mt7925.h
> @@ -375,6 +375,9 @@ int mt7925_mcu_set_sniffer(struct mt792x_dev *dev, struct ieee80211_vif *vif,
>  int mt7925_mcu_config_sniffer(struct mt792x_vif *vif,
>                               struct ieee80211_chanctx_conf *ctx);
>  int mt7925_mcu_get_temperature(struct mt792x_phy *phy);
> +int mt7925_mcu_get_chan_mib_info(struct mt792x_phy *phy, u8 band, bool add);
> +void mt7925_mac_update_chan_time(struct mt792x_phy *phy, bool reset);
> +void mt7925_update_channel(struct mt76_phy *mphy);
>
>  int mt7925_usb_sdio_tx_prepare_skb(struct mt76_dev *mdev, void *txwi_ptr,
>                                    enum mt76_txq_id qid, struct mt76_wcid *wcid,
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> index 0d477dc1086c..939a46c0428b 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> @@ -604,7 +604,7 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
>                 .sta_add = mt7925_mac_sta_add,
>                 .sta_event = mt7925_mac_sta_event,
>                 .sta_remove = mt7925_mac_sta_remove,
> -               .update_survey = mt792x_update_channel,
> +               .update_survey = mt7925_update_channel,
>         };
>         static const struct mt792x_hif_ops mt7925_pcie_ops = {
>                 .init_reset = mt7925e_init_reset,
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/usb.c b/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
> index f97f787b2198..2d3bf035fdfc 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/usb.c
> @@ -174,7 +174,7 @@ static int mt7925u_probe(struct usb_interface *usb_intf,
>                 .sta_add = mt7925_mac_sta_add,
>                 .sta_event = mt7925_mac_sta_event,
>                 .sta_remove = mt7925_mac_sta_remove,
> -               .update_survey = mt792x_update_channel,
> +               .update_survey = mt7925_update_channel,
>         };
>         static const struct mt792x_hif_ops hif_ops = {
>                 .mcu_init = mt7925u_mcu_init,
> diff --git a/drivers/net/wireless/mediatek/mt76/mt792x.h b/drivers/net/wireless/mediatek/mt76/mt792x.h
> index a7d71e007da0..3ee513258d00 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt792x.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt792x.h
> @@ -221,6 +221,7 @@ struct mt792x_phy {
>         u32 ampdu_ref;
>
>         struct mt76_mib_stats mib;
> +       struct mt76_channel_state state_ts[2];
>
>         u8 sta_work_count;
>         u8 clc_chan_conf;
> --
> 2.56.0
>



On Fri, Oct 23, 2026 at 2:05 PM Devin Wittmayer
<lucid_duck at justthetip.ca> wrote:
> Chris, here's the RFC you asked about. Busy time now follows the load
> and stays under the time on the channel, hopping every half second too.
[...]
> Could you try it on your A9000 against the Wi-Spy?

Thanks, Devin. It works here.

A9000 (mt7925u on USB 2.0), Ubuntu 7.0.0-38-generic, WM firmware
20260605184805 from Ubuntu's linux-firmware 20260319 (not the
2026-10-20 release), built out of tree from Ubuntu's mt76 source on
top of your survey v2. Ubuntu's 7.0 has no MT7927, so I dropped the
is_mt7927() block and the chan local it used; the rest is as sent.

Monitor mode, no association, 98 channels at 500 ms dwell, survey read
every ~5 s, 102 minutes: 8596 per-channel updates, none with busy over
active time, none going backwards. Quiet 6 GHz channels read 0.1-0.3%.
Without the patch the same setup gave 6013% on one channel. No MCU
timeouts or resets.

Load was a phone on a 2.4 GHz AP on channel 1 playing 2160p video.
Busy as a share of time on the channel, next to the Wi-Spy DBx2's RF
occupancy (bins 6 dB over its floor):

                              hopping                  locked on ch 1
                           busy      Wi-Spy     busy    Wi-Spy
  normal air         41.9%   71.4%       50.8%   69.1%
  2160p video     57.2%   85.5%       69.4%   85.0%

The Wi-Spy counts any energy, so it reads higher on 2.4 GHz, but the
two move together, and channels 2-5 rose with the load on both. On
5 GHz the Wi-Spy is the weaker of the two (3-16% on busy channels where
busy reads 13-32%), and busy stays above the airtime of the frames I
can decode there, as it should.

One thing on offset 20: in monitor mode it doesn't look like all of
rx. On busy 5 GHz channels it mostly reads under 2% while the frames I
decode alone take 13-19% (ch 149: 1.6% vs 19.1%), yet on 2.4 GHz
channels 2-4, between the networks on 1 and 6, it is nearly all of
busy (22-25%) with almost nothing decodable there. It does follow load
(ch 1 locked: 29% -> 55%). Busy doesn't depend on it, but MediaTek may
know what it counts. tx stays 0, as it should with nothing sent.

Tested-by: Chris Kelly <hoopyfrood42 at gmail.com>

Chris



More information about the Linux-mediatek mailing list