[PATCH v3 1/1] wifi: mt76: mt7921: fix txpower reporting from rate power configuration

Sean Wang sean.wang at kernel.org
Wed Mar 18 23:00:18 PDT 2026


Hi,

On Tue, Mar 17, 2026 at 12:30 PM Lucid Duck <lucid_duck at justthetip.ca> wrote:
>
> The mt7921 driver never updates phy->txpower_cur
> when TX power rate configuration is sent to firmware. This causes
> mt76_get_txpower() to report bogus values to userspace (typically
> 3 dBm) regardless of actual regulatory or SAR limits. User-set
> txpower limits via iw are also not reflected.
>
> Three root causes are addressed:
>
> 1. The rate power loop in mt76_connac_mcu_rate_txpower_band() computes
>    the correct bounded TX power for each channel but discards the return
>    value of mt76_get_rate_power_limits(). Fix: capture the return value
>    and store it to phy->txpower_cur when processing the current channel.
>
> 2. mt7921 uses the chanctx model but its add_chanctx callback bypasses
>    the common mt76_phy_update_channel(), leaving phy->chandef stale.
>    Fix: update phy->chandef from ctx->def in both add_chanctx and
>    change_chanctx, and trigger the rate power path to refresh
>    txpower_cur. Also trigger on IEEE80211_CONF_CHANGE_CHANNEL in
>    config(), matching mt7915.
>
> 3. For chanctx drivers, mac80211 routes user txpower changes through
>    BSS_CHANGED_TXPOWER in bss_info_changed() -- not through
>    IEEE80211_CONF_CHANGE_POWER in config(). hw->conf.power_level is
>    never updated. Fix: handle BSS_CHANGED_TXPOWER in
>    mt7921_bss_info_changed(), bridge bss_conf.txpower to
>    hw->conf.power_level, and re-trigger the rate power path.
>
> Tested on Alfa AWUS036AXML (MT7921AU), kernel 6.17.1-300.fc43:
>
>   Before: iw dev wlan0 info shows "txpower 3.00 dBm" (wrong)
>   After:  correct per-band values, user limits reflected
>
> Test results (regulatory domain: Canada/CA):
>   - 2.4GHz ch6:  33 dBm (30 dBm limit + 3 dBm 2x2 path delta)
>   - 5GHz ch36:   26 dBm (23 dBm limit + 3 dBm path delta)
>   - 6GHz ch5:    15 dBm (12 dBm limit + 3 dBm path delta)
>   - Band switch: 100 cycles, 0 failures
>   - Module reload: 50 cycles, 0 failures
>   - 2-hour soak: 480 samples, zero drift
>   - Regdomain switching: 10 countries, all correct
>   - User txpower limits: reflected on all bands
>   - Monitor mode: correct on all tested channels
>
> Signed-off-by: Lucid Duck <lucid_duck at justthetip.ca>
> ---
>  .../wireless/mediatek/mt76/mt76_connac_mcu.c  | 12 +++++++---
>  .../net/wireless/mediatek/mt76/mt7921/main.c  | 22 ++++++++++++++++++-
>  2 files changed, 30 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.c b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.c
> index 16db0f208..5856924a9 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.c
> @@ -2193,14 +2193,20 @@ mt76_connac_mcu_rate_txpower_band(struct mt76_phy *phy,
>                                 .hw_value = ch_list[idx],
>                                 .band = band,
>                         };
> -                       s8 reg_power, sar_power;
> +                       s8 reg_power, sar_power, max_power;
>
>                         reg_power = mt76_connac_get_ch_power(phy, &chan,
>                                                              tx_power);
>                         sar_power = mt76_get_sar_power(phy, &chan, reg_power);
>
> -                       mt76_get_rate_power_limits(phy, &chan, limits,
> -                                                  sar_power);
> +                       max_power = mt76_get_rate_power_limits(phy, &chan,
> +                                                              limits,
> +                                                              sar_power);
> +
> +                       if (phy->chandef.chan &&
> +                           phy->chandef.chan->hw_value == ch_list[idx] &&
> +                           phy->chandef.chan->band == band)
> +                               phy->txpower_cur = max_power;
>
>                         tx_power_tlv.last_msg = ch_list[idx] == last_ch;
>                         sku_tlbv.channel = ch_list[idx];
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/main.c b/drivers/net/wireless/mediatek/mt76/mt7921/main.c
> index 5881040ac..38a59c6f2 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7921/main.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7921/main.c
> @@ -638,7 +638,8 @@ static int mt7921_config(struct ieee80211_hw *hw, int radio_idx, u32 changed)
>
>         mt792x_mutex_acquire(dev);
>
> -       if (changed & IEEE80211_CONF_CHANGE_POWER) {
> +       if (changed & (IEEE80211_CONF_CHANGE_POWER |
> +                      IEEE80211_CONF_CHANGE_CHANNEL)) {
>                 ret = mt7921_set_tx_sar_pwr(hw, NULL);
>                 if (ret)
>                         goto out;
> @@ -719,6 +720,14 @@ static void mt7921_bss_info_changed(struct ieee80211_hw *hw,
>         if (changed & BSS_CHANGED_CQM)
>                 mt7921_mcu_set_rssimonitor(dev, vif);
>
> +       if (changed & BSS_CHANGED_TXPOWER) {
> +               int tx_power = info->txpower;
> +
> +               if (tx_power != INT_MIN && tx_power > 0)
> +                       hw->conf.power_level = tx_power;
> +               mt7921_set_tx_sar_pwr(hw, NULL);
> +       }
> +
>         if (changed & BSS_CHANGED_ASSOC) {
>                 mt7921_mcu_sta_update(dev, NULL, vif, true,
>                                       MT76_STA_INFO_STATE_ASSOC);
> @@ -1360,8 +1369,15 @@ mt7921_add_chanctx(struct ieee80211_hw *hw,
>                    struct ieee80211_chanctx_conf *ctx)
>  {
>         struct mt792x_dev *dev = mt792x_hw_dev(hw);
> +       struct mt76_phy *mphy = hw->priv;
>
>         dev->new_ctx = ctx;
> +       mphy->chandef = ctx->def;
> +
> +       mt792x_mutex_acquire(dev);
> +       mt7921_set_tx_sar_pwr(hw, NULL);
> +       mt792x_mutex_release(dev);
> +
>         return 0;
>  }
>
> @@ -1396,6 +1412,10 @@ mt7921_change_chanctx(struct ieee80211_hw *hw,
>                 mt7921_mcu_config_sniffer(mvif, ctx);
>         else
>                 mt76_connac_mcu_uni_set_chctx(mvif->phy->mt76, &mvif->bss_conf.mt76, ctx);
> +
> +       phy->mt76->chandef = ctx->def;
> +       mt7921_set_tx_sar_pwr(hw, NULL);
> +

I do not think the additional mt7921_set_tx_sar_pwr() calls are justified.

mt7921_set_tx_sar_pwr() is not a lightweight per-channel refresh. It
rebuilds and pushes the rate txpower table for all channels in the band
through the MCU path. This is appropriate when the underlying power
constraints change (e.g. SAR, regulatory limits, or user-configured
txpower), but not for pure channel/chanctx transitions.

add_chanctx(), change_chanctx(), and IEEE80211_CONF_CHANGE_CHANNEL only
reflect channel/context changes and do not imply that the firmware
txpower table needs to be recomputed. Using mt7921_set_tx_sar_pwr() here
effectively turns it into a catch-all sync path.

If the issue is stale txpower reporting after a channel switch, it should
be fixed by updating phy->txpower_cur from the already computed bounded
max power for the current channel, rather than re-triggering full table
programming on every chanctx/channel event.

BSS_CHANGED_TXPOWER is also problematic. It is a per-BSS (per-vif) event,
while hw->conf.power_level is shared per-HW state. Writing
info->txpower into hw->conf.power_level allows one interface to affect
the effective txpower of others sharing the same PHY, which breaks
multi-vif semantics.

>         mt792x_mutex_release(phy->dev);
>  }
>
> --
> 2.51.0
>



More information about the Linux-mediatek mailing list