| // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause |
| /* |
| * Copyright (C) 2024 - 2025 Intel Corporation |
| */ |
| #include <net/ip.h> |
| |
| #include "tx.h" |
| #include "sta.h" |
| #include "hcmd.h" |
| #include "iwl-utils.h" |
| #include "iface.h" |
| |
| #include "fw/dbg.h" |
| |
| #include "fw/api/tx.h" |
| #include "fw/api/rs.h" |
| #include "fw/api/txq.h" |
| #include "fw/api/datapath.h" |
| #include "fw/api/time-event.h" |
| |
| #define MAX_ANT_NUM 2 |
| |
| /* Toggles between TX antennas. Receives the bitmask of valid TX antennas and |
| * the *index* used for the last TX, and returns the next valid *index* to use. |
| * In order to set it in the tx_cmd, must do BIT(idx). |
| */ |
| static u8 iwl_mld_next_ant(u8 valid, u8 last_idx) |
| { |
| u8 index = last_idx; |
| |
| for (int i = 0; i < MAX_ANT_NUM; i++) { |
| index = (index + 1) % MAX_ANT_NUM; |
| if (valid & BIT(index)) |
| return index; |
| } |
| |
| WARN_ONCE(1, "Failed to toggle between antennas 0x%x", valid); |
| |
| return last_idx; |
| } |
| |
| void iwl_mld_toggle_tx_ant(struct iwl_mld *mld, u8 *ant) |
| { |
| *ant = iwl_mld_next_ant(iwl_mld_get_valid_tx_ant(mld), *ant); |
| } |
| |
| static int |
| iwl_mld_get_queue_size(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| struct ieee80211_sta *sta = txq->sta; |
| struct ieee80211_link_sta *link_sta; |
| unsigned int link_id; |
| int max_size = IWL_DEFAULT_QUEUE_SIZE; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| for_each_sta_active_link(txq->vif, sta, link_sta, link_id) { |
| if (link_sta->eht_cap.has_eht) { |
| max_size = IWL_DEFAULT_QUEUE_SIZE_EHT; |
| break; |
| } |
| |
| if (link_sta->he_cap.has_he) |
| max_size = IWL_DEFAULT_QUEUE_SIZE_HE; |
| } |
| |
| return max_size; |
| } |
| |
| static int iwl_mld_allocate_txq(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| u8 tid = txq->tid == IEEE80211_NUM_TIDS ? IWL_MGMT_TID : txq->tid; |
| u32 fw_sta_mask = iwl_mld_fw_sta_id_mask(mld, txq->sta); |
| /* We can't know when the station is asleep or awake, so we |
| * must disable the queue hang detection. |
| */ |
| unsigned int watchdog_timeout = txq->vif->type == NL80211_IFTYPE_AP ? |
| IWL_WATCHDOG_DISABLED : |
| mld->trans->mac_cfg->base->wd_timeout; |
| int queue, size; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| if (tid == IWL_MGMT_TID) |
| size = max_t(u32, IWL_MGMT_QUEUE_SIZE, |
| mld->trans->mac_cfg->base->min_txq_size); |
| else |
| size = iwl_mld_get_queue_size(mld, txq); |
| |
| queue = iwl_trans_txq_alloc(mld->trans, 0, fw_sta_mask, tid, size, |
| watchdog_timeout); |
| |
| if (queue >= 0) |
| IWL_DEBUG_TX_QUEUES(mld, |
| "Enabling TXQ #%d for sta mask 0x%x tid %d\n", |
| queue, fw_sta_mask, tid); |
| return queue; |
| } |
| |
| static int iwl_mld_add_txq(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| struct iwl_mld_txq *mld_txq = iwl_mld_txq_from_mac80211(txq); |
| int id; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| /* This will alse send the SCD_QUEUE_CONFIG_CMD */ |
| id = iwl_mld_allocate_txq(mld, txq); |
| if (id < 0) |
| return id; |
| |
| mld_txq->fw_id = id; |
| mld_txq->status.allocated = true; |
| |
| rcu_assign_pointer(mld->fw_id_to_txq[id], txq); |
| |
| return 0; |
| } |
| |
| void iwl_mld_add_txq_list(struct iwl_mld *mld) |
| { |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| while (!list_empty(&mld->txqs_to_add)) { |
| struct ieee80211_txq *txq; |
| struct iwl_mld_txq *mld_txq = |
| list_first_entry(&mld->txqs_to_add, struct iwl_mld_txq, |
| list); |
| int failed; |
| |
| txq = container_of((void *)mld_txq, struct ieee80211_txq, |
| drv_priv); |
| |
| failed = iwl_mld_add_txq(mld, txq); |
| |
| local_bh_disable(); |
| spin_lock(&mld->add_txqs_lock); |
| list_del_init(&mld_txq->list); |
| spin_unlock(&mld->add_txqs_lock); |
| /* If the queue allocation failed, we can't transmit. Leave the |
| * frames on the txq, maybe the attempt to allocate the queue |
| * will succeed. |
| */ |
| if (!failed) |
| iwl_mld_tx_from_txq(mld, txq); |
| local_bh_enable(); |
| } |
| } |
| |
| void iwl_mld_add_txqs_wk(struct wiphy *wiphy, struct wiphy_work *wk) |
| { |
| struct iwl_mld *mld = container_of(wk, struct iwl_mld, |
| add_txqs_wk); |
| |
| /* will reschedule to run after restart */ |
| if (mld->fw_status.in_hw_restart) |
| return; |
| |
| iwl_mld_add_txq_list(mld); |
| } |
| |
| void |
| iwl_mld_free_txq(struct iwl_mld *mld, u32 fw_sta_mask, u32 tid, u32 queue_id) |
| { |
| struct iwl_scd_queue_cfg_cmd remove_cmd = { |
| .operation = cpu_to_le32(IWL_SCD_QUEUE_REMOVE), |
| .u.remove.tid = cpu_to_le32(tid), |
| .u.remove.sta_mask = cpu_to_le32(fw_sta_mask), |
| }; |
| |
| iwl_mld_send_cmd_pdu(mld, |
| WIDE_ID(DATA_PATH_GROUP, SCD_QUEUE_CONFIG_CMD), |
| &remove_cmd); |
| |
| iwl_trans_txq_free(mld->trans, queue_id); |
| } |
| |
| void iwl_mld_remove_txq(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| struct iwl_mld_txq *mld_txq = iwl_mld_txq_from_mac80211(txq); |
| u32 sta_msk, tid; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| spin_lock_bh(&mld->add_txqs_lock); |
| if (!list_empty(&mld_txq->list)) |
| list_del_init(&mld_txq->list); |
| spin_unlock_bh(&mld->add_txqs_lock); |
| |
| if (!mld_txq->status.allocated || |
| WARN_ON(mld_txq->fw_id >= ARRAY_SIZE(mld->fw_id_to_txq))) |
| return; |
| |
| sta_msk = iwl_mld_fw_sta_id_mask(mld, txq->sta); |
| |
| tid = txq->tid == IEEE80211_NUM_TIDS ? IWL_MGMT_TID : |
| txq->tid; |
| |
| iwl_mld_free_txq(mld, sta_msk, tid, mld_txq->fw_id); |
| |
| RCU_INIT_POINTER(mld->fw_id_to_txq[mld_txq->fw_id], NULL); |
| mld_txq->status.allocated = false; |
| } |
| |
| #define OPT_HDR(type, skb, off) \ |
| (type *)(skb_network_header(skb) + (off)) |
| |
| static __le32 |
| iwl_mld_get_offload_assist(struct sk_buff *skb, bool amsdu) |
| { |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| u16 mh_len = ieee80211_hdrlen(hdr->frame_control); |
| u16 offload_assist = 0; |
| #if IS_ENABLED(CONFIG_INET) |
| u8 protocol = 0; |
| |
| /* Do not compute checksum if already computed */ |
| if (skb->ip_summed != CHECKSUM_PARTIAL) |
| goto out; |
| |
| /* We do not expect to be requested to csum stuff we do not support */ |
| |
| /* TBD: do we also need to check |
| * !(mvm->hw->netdev_features & IWL_TX_CSUM_NETIF_FLAGS) now that all |
| * the devices we support has this flags? |
| */ |
| if (WARN_ONCE(skb->protocol != htons(ETH_P_IP) && |
| skb->protocol != htons(ETH_P_IPV6), |
| "No support for requested checksum\n")) { |
| skb_checksum_help(skb); |
| goto out; |
| } |
| |
| if (skb->protocol == htons(ETH_P_IP)) { |
| protocol = ip_hdr(skb)->protocol; |
| } else { |
| #if IS_ENABLED(CONFIG_IPV6) |
| struct ipv6hdr *ipv6h = |
| (struct ipv6hdr *)skb_network_header(skb); |
| unsigned int off = sizeof(*ipv6h); |
| |
| protocol = ipv6h->nexthdr; |
| while (protocol != NEXTHDR_NONE && ipv6_ext_hdr(protocol)) { |
| struct ipv6_opt_hdr *hp; |
| |
| /* only supported extension headers */ |
| if (protocol != NEXTHDR_ROUTING && |
| protocol != NEXTHDR_HOP && |
| protocol != NEXTHDR_DEST) { |
| skb_checksum_help(skb); |
| goto out; |
| } |
| |
| hp = OPT_HDR(struct ipv6_opt_hdr, skb, off); |
| protocol = hp->nexthdr; |
| off += ipv6_optlen(hp); |
| } |
| /* if we get here - protocol now should be TCP/UDP */ |
| #endif |
| } |
| |
| if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP) { |
| WARN_ON_ONCE(1); |
| skb_checksum_help(skb); |
| goto out; |
| } |
| |
| /* enable L4 csum */ |
| offload_assist |= BIT(TX_CMD_OFFLD_L4_EN); |
| |
| /* Set offset to IP header (snap). |
| * We don't support tunneling so no need to take care of inner header. |
| * Size is in words. |
| */ |
| offload_assist |= (4 << TX_CMD_OFFLD_IP_HDR); |
| |
| /* Do IPv4 csum for AMSDU only (no IP csum for Ipv6) */ |
| if (skb->protocol == htons(ETH_P_IP) && amsdu) { |
| ip_hdr(skb)->check = 0; |
| offload_assist |= BIT(TX_CMD_OFFLD_L3_EN); |
| } |
| |
| /* reset UDP/TCP header csum */ |
| if (protocol == IPPROTO_TCP) |
| tcp_hdr(skb)->check = 0; |
| else |
| udp_hdr(skb)->check = 0; |
| |
| out: |
| #endif |
| mh_len /= 2; |
| offload_assist |= mh_len << TX_CMD_OFFLD_MH_SIZE; |
| |
| if (amsdu) |
| offload_assist |= BIT(TX_CMD_OFFLD_AMSDU); |
| else if (ieee80211_hdrlen(hdr->frame_control) % 4) |
| /* padding is inserted later in transport */ |
| offload_assist |= BIT(TX_CMD_OFFLD_PAD); |
| |
| return cpu_to_le32(offload_assist); |
| } |
| |
| static void iwl_mld_get_basic_rates_and_band(struct iwl_mld *mld, |
| struct ieee80211_vif *vif, |
| struct ieee80211_tx_info *info, |
| unsigned long *basic_rates, |
| u8 *band) |
| { |
| u32 link_id = u32_get_bits(info->control.flags, |
| IEEE80211_TX_CTRL_MLO_LINK); |
| |
| *basic_rates = vif->bss_conf.basic_rates; |
| *band = info->band; |
| |
| if (link_id == IEEE80211_LINK_UNSPECIFIED && |
| ieee80211_vif_is_mld(vif)) { |
| /* shouldn't do this when >1 link is active */ |
| WARN_ON(hweight16(vif->active_links) != 1); |
| link_id = __ffs(vif->active_links); |
| } |
| |
| if (link_id < IEEE80211_LINK_UNSPECIFIED) { |
| struct ieee80211_bss_conf *link_conf; |
| |
| rcu_read_lock(); |
| link_conf = rcu_dereference(vif->link_conf[link_id]); |
| if (link_conf) { |
| *basic_rates = link_conf->basic_rates; |
| if (link_conf->chanreq.oper.chan) |
| *band = link_conf->chanreq.oper.chan->band; |
| } |
| rcu_read_unlock(); |
| } |
| } |
| |
| u8 iwl_mld_get_lowest_rate(struct iwl_mld *mld, |
| struct ieee80211_tx_info *info, |
| struct ieee80211_vif *vif) |
| { |
| struct ieee80211_supported_band *sband; |
| u16 lowest_cck = IWL_RATE_COUNT, lowest_ofdm = IWL_RATE_COUNT; |
| unsigned long basic_rates; |
| u8 band, rate; |
| u32 i; |
| |
| iwl_mld_get_basic_rates_and_band(mld, vif, info, &basic_rates, &band); |
| |
| sband = mld->hw->wiphy->bands[band]; |
| for_each_set_bit(i, &basic_rates, BITS_PER_LONG) { |
| u16 hw = sband->bitrates[i].hw_value; |
| |
| if (hw >= IWL_FIRST_OFDM_RATE) { |
| if (lowest_ofdm > hw) |
| lowest_ofdm = hw; |
| } else if (lowest_cck > hw) { |
| lowest_cck = hw; |
| } |
| } |
| |
| if (band == NL80211_BAND_2GHZ && !vif->p2p && |
| vif->type != NL80211_IFTYPE_P2P_DEVICE && |
| !(info->flags & IEEE80211_TX_CTL_NO_CCK_RATE)) { |
| if (lowest_cck != IWL_RATE_COUNT) |
| rate = lowest_cck; |
| else if (lowest_ofdm != IWL_RATE_COUNT) |
| rate = lowest_ofdm; |
| else |
| rate = IWL_FIRST_CCK_RATE; |
| } else if (lowest_ofdm != IWL_RATE_COUNT) { |
| rate = lowest_ofdm; |
| } else { |
| rate = IWL_FIRST_OFDM_RATE; |
| } |
| |
| return rate; |
| } |
| |
| static u32 iwl_mld_mac80211_rate_idx_to_fw(struct iwl_mld *mld, |
| struct ieee80211_tx_info *info, |
| int rate_idx) |
| { |
| u32 rate_flags = 0; |
| u8 rate_plcp; |
| |
| /* if the rate isn't a well known legacy rate, take the lowest one */ |
| if (rate_idx < 0 || rate_idx >= IWL_RATE_COUNT_LEGACY) |
| rate_idx = iwl_mld_get_lowest_rate(mld, info, |
| info->control.vif); |
| |
| WARN_ON_ONCE(rate_idx < 0); |
| |
| /* Set CCK or OFDM flag */ |
| if (rate_idx <= IWL_LAST_CCK_RATE) |
| rate_flags |= RATE_MCS_MOD_TYPE_CCK; |
| else |
| rate_flags |= RATE_MCS_MOD_TYPE_LEGACY_OFDM; |
| |
| /* Legacy rates are indexed: |
| * 0 - 3 for CCK and 0 - 7 for OFDM |
| */ |
| rate_plcp = (rate_idx >= IWL_FIRST_OFDM_RATE ? |
| rate_idx - IWL_FIRST_OFDM_RATE : rate_idx); |
| |
| return (u32)rate_plcp | rate_flags; |
| } |
| |
| static u32 iwl_mld_get_tx_ant(struct iwl_mld *mld, |
| struct ieee80211_tx_info *info, |
| struct ieee80211_sta *sta, __le16 fc) |
| { |
| if (sta && ieee80211_is_data(fc)) { |
| struct iwl_mld_sta *mld_sta = iwl_mld_sta_from_mac80211(sta); |
| |
| return BIT(mld_sta->data_tx_ant) << RATE_MCS_ANT_POS; |
| } |
| |
| return BIT(mld->mgmt_tx_ant) << RATE_MCS_ANT_POS; |
| } |
| |
| static u32 iwl_mld_get_inject_tx_rate(struct iwl_mld *mld, |
| struct ieee80211_tx_info *info, |
| struct ieee80211_sta *sta, |
| __le16 fc) |
| { |
| struct ieee80211_tx_rate *rate = &info->control.rates[0]; |
| u32 result; |
| |
| if (rate->flags & IEEE80211_TX_RC_VHT_MCS) { |
| u8 mcs = ieee80211_rate_get_vht_mcs(rate); |
| u8 nss = ieee80211_rate_get_vht_nss(rate); |
| |
| result = RATE_MCS_MOD_TYPE_VHT; |
| result |= u32_encode_bits(mcs, RATE_MCS_CODE_MSK); |
| result |= u32_encode_bits(nss, RATE_MCS_NSS_MSK); |
| |
| if (rate->flags & IEEE80211_TX_RC_SHORT_GI) |
| result |= RATE_MCS_SGI_MSK; |
| |
| if (rate->flags & IEEE80211_TX_RC_40_MHZ_WIDTH) |
| result |= RATE_MCS_CHAN_WIDTH_40; |
| else if (rate->flags & IEEE80211_TX_RC_80_MHZ_WIDTH) |
| result |= RATE_MCS_CHAN_WIDTH_80; |
| else if (rate->flags & IEEE80211_TX_RC_160_MHZ_WIDTH) |
| result |= RATE_MCS_CHAN_WIDTH_160; |
| } else if (rate->flags & IEEE80211_TX_RC_MCS) { |
| /* only MCS 0-15 are supported */ |
| u8 mcs = rate->idx & 7; |
| u8 nss = rate->idx > 7; |
| |
| result = RATE_MCS_MOD_TYPE_HT; |
| result |= u32_encode_bits(mcs, RATE_MCS_CODE_MSK); |
| result |= u32_encode_bits(nss, RATE_MCS_NSS_MSK); |
| |
| if (rate->flags & IEEE80211_TX_RC_SHORT_GI) |
| result |= RATE_MCS_SGI_MSK; |
| if (rate->flags & IEEE80211_TX_RC_40_MHZ_WIDTH) |
| result |= RATE_MCS_CHAN_WIDTH_40; |
| if (info->flags & IEEE80211_TX_CTL_LDPC) |
| result |= RATE_MCS_LDPC_MSK; |
| if (u32_get_bits(info->flags, IEEE80211_TX_CTL_STBC)) |
| result |= RATE_MCS_STBC_MSK; |
| } else { |
| result = iwl_mld_mac80211_rate_idx_to_fw(mld, info, rate->idx); |
| } |
| |
| if (info->control.antennas) |
| result |= u32_encode_bits(info->control.antennas, |
| RATE_MCS_ANT_AB_MSK); |
| else |
| result |= iwl_mld_get_tx_ant(mld, info, sta, fc); |
| |
| return result; |
| } |
| |
| static __le32 iwl_mld_get_tx_rate_n_flags(struct iwl_mld *mld, |
| struct ieee80211_tx_info *info, |
| struct ieee80211_sta *sta, __le16 fc) |
| { |
| u32 rate; |
| |
| if (unlikely(info->control.flags & IEEE80211_TX_CTRL_RATE_INJECT)) |
| rate = iwl_mld_get_inject_tx_rate(mld, info, sta, fc); |
| else |
| rate = iwl_mld_mac80211_rate_idx_to_fw(mld, info, -1) | |
| iwl_mld_get_tx_ant(mld, info, sta, fc); |
| |
| return iwl_v3_rate_to_v2_v3(rate, mld->fw_rates_ver_3); |
| } |
| |
| static void |
| iwl_mld_fill_tx_cmd_hdr(struct iwl_tx_cmd *tx_cmd, |
| struct sk_buff *skb, bool amsdu) |
| { |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| struct ieee80211_vif *vif; |
| |
| /* Copy MAC header from skb into command buffer */ |
| memcpy(tx_cmd->hdr, hdr, ieee80211_hdrlen(hdr->frame_control)); |
| |
| if (!amsdu || !skb_is_gso(skb)) |
| return; |
| |
| /* As described in IEEE sta 802.11-2020, table 9-30 (Address |
| * field contents), A-MSDU address 3 should contain the BSSID |
| * address. |
| * |
| * In TSO, the skb header address 3 contains the original address 3 to |
| * correctly create all the A-MSDU subframes headers from it. |
| * Override now the address 3 in the command header with the BSSID. |
| * |
| * Note: we fill in the MLD address, but the firmware will do the |
| * necessary translation to link address after encryption. |
| */ |
| vif = info->control.vif; |
| switch (vif->type) { |
| case NL80211_IFTYPE_STATION: |
| ether_addr_copy(tx_cmd->hdr->addr3, vif->cfg.ap_addr); |
| break; |
| case NL80211_IFTYPE_AP: |
| ether_addr_copy(tx_cmd->hdr->addr3, vif->addr); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| static void |
| iwl_mld_fill_tx_cmd(struct iwl_mld *mld, struct sk_buff *skb, |
| struct iwl_device_tx_cmd *dev_tx_cmd, |
| struct ieee80211_sta *sta) |
| { |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| struct iwl_mld_sta *mld_sta = sta ? iwl_mld_sta_from_mac80211(sta) : |
| NULL; |
| struct iwl_tx_cmd *tx_cmd; |
| bool amsdu = ieee80211_is_data_qos(hdr->frame_control) && |
| (*ieee80211_get_qos_ctl(hdr) & |
| IEEE80211_QOS_CTL_A_MSDU_PRESENT); |
| __le32 rate_n_flags = 0; |
| u16 flags = 0; |
| |
| dev_tx_cmd->hdr.cmd = TX_CMD; |
| |
| if (!info->control.hw_key) |
| flags |= IWL_TX_FLAGS_ENCRYPT_DIS; |
| |
| /* For data and mgmt packets rate info comes from the fw. |
| * Only set rate/antenna for injected frames with fixed rate, or |
| * when no sta is given. |
| */ |
| if (unlikely(!sta || |
| info->control.flags & IEEE80211_TX_CTRL_RATE_INJECT)) { |
| flags |= IWL_TX_FLAGS_CMD_RATE; |
| rate_n_flags = iwl_mld_get_tx_rate_n_flags(mld, info, sta, |
| hdr->frame_control); |
| } else if (!ieee80211_is_data(hdr->frame_control) || |
| (mld_sta && |
| mld_sta->sta_state < IEEE80211_STA_AUTHORIZED)) { |
| /* These are important frames */ |
| flags |= IWL_TX_FLAGS_HIGH_PRI; |
| } |
| |
| tx_cmd = (void *)dev_tx_cmd->payload; |
| |
| iwl_mld_fill_tx_cmd_hdr(tx_cmd, skb, amsdu); |
| |
| tx_cmd->offload_assist = iwl_mld_get_offload_assist(skb, amsdu); |
| |
| /* Total # bytes to be transmitted */ |
| tx_cmd->len = cpu_to_le16((u16)skb->len); |
| |
| tx_cmd->flags = cpu_to_le16(flags); |
| |
| tx_cmd->rate_n_flags = rate_n_flags; |
| } |
| |
| /* Caller of this need to check that info->control.vif is not NULL */ |
| static struct iwl_mld_link * |
| iwl_mld_get_link_from_tx_info(struct ieee80211_tx_info *info) |
| { |
| struct iwl_mld_vif *mld_vif = |
| iwl_mld_vif_from_mac80211(info->control.vif); |
| u32 link_id = u32_get_bits(info->control.flags, |
| IEEE80211_TX_CTRL_MLO_LINK); |
| |
| if (link_id == IEEE80211_LINK_UNSPECIFIED) { |
| if (info->control.vif->active_links) |
| link_id = ffs(info->control.vif->active_links) - 1; |
| else |
| link_id = 0; |
| } |
| |
| return rcu_dereference(mld_vif->link[link_id]); |
| } |
| |
| static int |
| iwl_mld_get_tx_queue_id(struct iwl_mld *mld, struct ieee80211_txq *txq, |
| struct sk_buff *skb) |
| { |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| __le16 fc = hdr->frame_control; |
| struct iwl_mld_vif *mld_vif; |
| struct iwl_mld_link *link; |
| |
| if (txq && txq->sta) |
| return iwl_mld_txq_from_mac80211(txq)->fw_id; |
| |
| if (!info->control.vif) |
| return IWL_MLD_INVALID_QUEUE; |
| |
| switch (info->control.vif->type) { |
| case NL80211_IFTYPE_AP: |
| case NL80211_IFTYPE_ADHOC: |
| link = iwl_mld_get_link_from_tx_info(info); |
| |
| if (WARN_ON(!link)) |
| break; |
| |
| /* ucast disassociate/deauth frames without a station might |
| * happen, especially with reason 7 ("Class 3 frame received |
| * from nonassociated STA"). |
| */ |
| if (ieee80211_is_mgmt(fc) && |
| (!ieee80211_is_bufferable_mmpdu(skb) || |
| ieee80211_is_deauth(fc) || ieee80211_is_disassoc(fc))) |
| return link->bcast_sta.queue_id; |
| |
| if (is_multicast_ether_addr(hdr->addr1) && |
| !ieee80211_has_order(fc)) |
| return link->mcast_sta.queue_id; |
| |
| WARN_ONCE(info->control.vif->type != NL80211_IFTYPE_ADHOC, |
| "Couldn't find a TXQ. fc=0x%02x", le16_to_cpu(fc)); |
| return link->bcast_sta.queue_id; |
| case NL80211_IFTYPE_P2P_DEVICE: |
| mld_vif = iwl_mld_vif_from_mac80211(info->control.vif); |
| |
| if (mld_vif->roc_activity != ROC_ACTIVITY_P2P_DISC && |
| mld_vif->roc_activity != ROC_ACTIVITY_P2P_NEG) { |
| IWL_DEBUG_DROP(mld, |
| "Drop tx outside ROC with activity %d\n", |
| mld_vif->roc_activity); |
| return IWL_MLD_INVALID_DROP_TX; |
| } |
| |
| WARN_ON(!ieee80211_is_mgmt(fc)); |
| |
| return mld_vif->aux_sta.queue_id; |
| case NL80211_IFTYPE_MONITOR: |
| mld_vif = iwl_mld_vif_from_mac80211(info->control.vif); |
| return mld_vif->deflink.mon_sta.queue_id; |
| case NL80211_IFTYPE_STATION: |
| mld_vif = iwl_mld_vif_from_mac80211(info->control.vif); |
| |
| if (!(info->flags & IEEE80211_TX_CTL_TX_OFFCHAN)) { |
| IWL_DEBUG_DROP(mld, "Drop tx not off-channel\n"); |
| return IWL_MLD_INVALID_DROP_TX; |
| } |
| |
| if (mld_vif->roc_activity != ROC_ACTIVITY_HOTSPOT) { |
| IWL_DEBUG_DROP(mld, "Drop tx outside ROC\n"); |
| return IWL_MLD_INVALID_DROP_TX; |
| } |
| |
| WARN_ON(!ieee80211_is_mgmt(fc)); |
| return mld_vif->aux_sta.queue_id; |
| default: |
| WARN_ONCE(1, "Unsupported vif type\n"); |
| break; |
| } |
| |
| return IWL_MLD_INVALID_QUEUE; |
| } |
| |
| static void iwl_mld_probe_resp_set_noa(struct iwl_mld *mld, |
| struct sk_buff *skb) |
| { |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct iwl_mld_link *mld_link = |
| &iwl_mld_vif_from_mac80211(info->control.vif)->deflink; |
| struct iwl_probe_resp_data *resp_data; |
| u8 *pos; |
| |
| if (!info->control.vif->p2p) |
| return; |
| |
| rcu_read_lock(); |
| |
| resp_data = rcu_dereference(mld_link->probe_resp_data); |
| if (!resp_data) |
| goto out; |
| |
| if (!resp_data->notif.noa_active) |
| goto out; |
| |
| if (skb_tailroom(skb) < resp_data->noa_len) { |
| if (pskb_expand_head(skb, 0, resp_data->noa_len, GFP_ATOMIC)) { |
| IWL_ERR(mld, |
| "Failed to reallocate probe resp\n"); |
| goto out; |
| } |
| } |
| |
| pos = skb_put(skb, resp_data->noa_len); |
| |
| *pos++ = WLAN_EID_VENDOR_SPECIFIC; |
| /* Set length of IE body (not including ID and length itself) */ |
| *pos++ = resp_data->noa_len - 2; |
| *pos++ = (WLAN_OUI_WFA >> 16) & 0xff; |
| *pos++ = (WLAN_OUI_WFA >> 8) & 0xff; |
| *pos++ = WLAN_OUI_WFA & 0xff; |
| *pos++ = WLAN_OUI_TYPE_WFA_P2P; |
| |
| memcpy(pos, &resp_data->notif.noa_attr, |
| resp_data->noa_len - sizeof(struct ieee80211_vendor_ie)); |
| |
| out: |
| rcu_read_unlock(); |
| } |
| |
| /* This function must be called with BHs disabled */ |
| static int iwl_mld_tx_mpdu(struct iwl_mld *mld, struct sk_buff *skb, |
| struct ieee80211_txq *txq) |
| { |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct ieee80211_sta *sta = txq ? txq->sta : NULL; |
| struct iwl_device_tx_cmd *dev_tx_cmd; |
| int queue = iwl_mld_get_tx_queue_id(mld, txq, skb); |
| u8 tid = IWL_MAX_TID_COUNT; |
| |
| if (WARN_ONCE(queue == IWL_MLD_INVALID_QUEUE, "Invalid TX Queue id") || |
| queue == IWL_MLD_INVALID_DROP_TX) |
| return -1; |
| |
| if (unlikely(ieee80211_is_any_nullfunc(hdr->frame_control))) |
| return -1; |
| |
| dev_tx_cmd = iwl_trans_alloc_tx_cmd(mld->trans); |
| if (unlikely(!dev_tx_cmd)) |
| return -1; |
| |
| if (unlikely(ieee80211_is_probe_resp(hdr->frame_control))) { |
| if (IWL_MLD_NON_TRANSMITTING_AP) |
| return -1; |
| |
| iwl_mld_probe_resp_set_noa(mld, skb); |
| } |
| |
| iwl_mld_fill_tx_cmd(mld, skb, dev_tx_cmd, sta); |
| |
| if (ieee80211_is_data(hdr->frame_control)) { |
| if (ieee80211_is_data_qos(hdr->frame_control)) |
| tid = ieee80211_get_tid(hdr); |
| else |
| tid = IWL_TID_NON_QOS; |
| } |
| |
| IWL_DEBUG_TX(mld, "TX TID:%d from Q:%d len %d\n", |
| tid, queue, skb->len); |
| |
| /* From now on, we cannot access info->control */ |
| memset(&info->status, 0, sizeof(info->status)); |
| memset(info->driver_data, 0, sizeof(info->driver_data)); |
| |
| info->driver_data[1] = dev_tx_cmd; |
| |
| if (iwl_trans_tx(mld->trans, skb, dev_tx_cmd, queue)) |
| goto err; |
| |
| /* Update low-latency counter when a packet is queued instead |
| * of after TX, it makes sense for early low-latency detection |
| */ |
| if (sta) |
| iwl_mld_low_latency_update_counters(mld, hdr, sta, 0); |
| |
| return 0; |
| |
| err: |
| iwl_trans_free_tx_cmd(mld->trans, dev_tx_cmd); |
| IWL_DEBUG_TX(mld, "TX from Q:%d dropped\n", queue); |
| return -1; |
| } |
| |
| #ifdef CONFIG_INET |
| |
| /* This function handles the segmentation of a large TSO packet into multiple |
| * MPDUs, ensuring that the resulting segments conform to AMSDU limits and |
| * constraints. |
| */ |
| static int iwl_mld_tx_tso_segment(struct iwl_mld *mld, struct sk_buff *skb, |
| struct ieee80211_sta *sta, |
| struct sk_buff_head *mpdus_skbs) |
| { |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| netdev_features_t netdev_flags = NETIF_F_CSUM_MASK | NETIF_F_SG; |
| unsigned int mss = skb_shinfo(skb)->gso_size; |
| unsigned int num_subframes, tcp_payload_len, subf_len; |
| u16 snap_ip_tcp, pad, max_tid_amsdu_len; |
| u8 tid; |
| |
| snap_ip_tcp = 8 + skb_network_header_len(skb) + tcp_hdrlen(skb); |
| |
| if (!ieee80211_is_data_qos(hdr->frame_control) || |
| !sta->cur->max_rc_amsdu_len) |
| return iwl_tx_tso_segment(skb, 1, netdev_flags, mpdus_skbs); |
| |
| /* Do not build AMSDU for IPv6 with extension headers. |
| * Ask stack to segment and checksum the generated MPDUs for us. |
| */ |
| if (skb->protocol == htons(ETH_P_IPV6) && |
| ((struct ipv6hdr *)skb_network_header(skb))->nexthdr != |
| IPPROTO_TCP) { |
| netdev_flags &= ~NETIF_F_CSUM_MASK; |
| return iwl_tx_tso_segment(skb, 1, netdev_flags, mpdus_skbs); |
| } |
| |
| tid = ieee80211_get_tid(hdr); |
| if (WARN_ON_ONCE(tid >= IWL_MAX_TID_COUNT)) |
| return -EINVAL; |
| |
| max_tid_amsdu_len = sta->cur->max_tid_amsdu_len[tid]; |
| if (!max_tid_amsdu_len) |
| return iwl_tx_tso_segment(skb, 1, netdev_flags, mpdus_skbs); |
| |
| /* Sub frame header + SNAP + IP header + TCP header + MSS */ |
| subf_len = sizeof(struct ethhdr) + snap_ip_tcp + mss; |
| pad = (4 - subf_len) & 0x3; |
| |
| /* If we have N subframes in the A-MSDU, then the A-MSDU's size is |
| * N * subf_len + (N - 1) * pad. |
| */ |
| num_subframes = (max_tid_amsdu_len + pad) / (subf_len + pad); |
| |
| if (sta->max_amsdu_subframes && |
| num_subframes > sta->max_amsdu_subframes) |
| num_subframes = sta->max_amsdu_subframes; |
| |
| tcp_payload_len = skb_tail_pointer(skb) - skb_transport_header(skb) - |
| tcp_hdrlen(skb) + skb->data_len; |
| |
| /* Make sure we have enough TBs for the A-MSDU: |
| * 2 for each subframe |
| * 1 more for each fragment |
| * 1 more for the potential data in the header |
| */ |
| if ((num_subframes * 2 + skb_shinfo(skb)->nr_frags + 1) > |
| mld->trans->info.max_skb_frags) |
| num_subframes = 1; |
| |
| if (num_subframes > 1) |
| *ieee80211_get_qos_ctl(hdr) |= IEEE80211_QOS_CTL_A_MSDU_PRESENT; |
| |
| /* This skb fits in one single A-MSDU */ |
| if (tcp_payload_len <= num_subframes * mss) { |
| __skb_queue_tail(mpdus_skbs, skb); |
| return 0; |
| } |
| |
| /* Trick the segmentation function to make it create SKBs that can fit |
| * into one A-MSDU. |
| */ |
| return iwl_tx_tso_segment(skb, num_subframes, netdev_flags, mpdus_skbs); |
| } |
| |
| /* Manages TSO (TCP Segmentation Offload) packet transmission by segmenting |
| * large packets when necessary and transmitting each segment as MPDU. |
| */ |
| static int iwl_mld_tx_tso(struct iwl_mld *mld, struct sk_buff *skb, |
| struct ieee80211_txq *txq) |
| { |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct sk_buff *orig_skb = skb; |
| struct sk_buff_head mpdus_skbs; |
| unsigned int payload_len; |
| int ret; |
| |
| if (WARN_ON(!txq || !txq->sta)) |
| return -1; |
| |
| payload_len = skb_tail_pointer(skb) - skb_transport_header(skb) - |
| tcp_hdrlen(skb) + skb->data_len; |
| |
| if (payload_len <= skb_shinfo(skb)->gso_size) |
| return iwl_mld_tx_mpdu(mld, skb, txq); |
| |
| if (!info->control.vif) |
| return -1; |
| |
| __skb_queue_head_init(&mpdus_skbs); |
| |
| ret = iwl_mld_tx_tso_segment(mld, skb, txq->sta, &mpdus_skbs); |
| if (ret) |
| return ret; |
| |
| WARN_ON(skb_queue_empty(&mpdus_skbs)); |
| |
| while (!skb_queue_empty(&mpdus_skbs)) { |
| skb = __skb_dequeue(&mpdus_skbs); |
| |
| ret = iwl_mld_tx_mpdu(mld, skb, txq); |
| if (!ret) |
| continue; |
| |
| /* Free skbs created as part of TSO logic that have not yet |
| * been dequeued |
| */ |
| __skb_queue_purge(&mpdus_skbs); |
| |
| /* skb here is not necessarily same as skb that entered |
| * this method, so free it explicitly. |
| */ |
| if (skb == orig_skb) |
| ieee80211_free_txskb(mld->hw, skb); |
| else |
| kfree_skb(skb); |
| |
| /* there was error, but we consumed skb one way or |
| * another, so return 0 |
| */ |
| return 0; |
| } |
| |
| return 0; |
| } |
| #else |
| static int iwl_mld_tx_tso(struct iwl_mld *mld, struct sk_buff *skb, |
| struct ieee80211_txq *txq) |
| { |
| /* Impossible to get TSO without CONFIG_INET */ |
| WARN_ON(1); |
| |
| return -1; |
| } |
| #endif /* CONFIG_INET */ |
| |
| void iwl_mld_tx_skb(struct iwl_mld *mld, struct sk_buff *skb, |
| struct ieee80211_txq *txq) |
| { |
| if (skb_is_gso(skb)) { |
| if (!iwl_mld_tx_tso(mld, skb, txq)) |
| return; |
| goto err; |
| } |
| |
| if (likely(!iwl_mld_tx_mpdu(mld, skb, txq))) |
| return; |
| |
| err: |
| ieee80211_free_txskb(mld->hw, skb); |
| } |
| |
| void iwl_mld_tx_from_txq(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| struct iwl_mld_txq *mld_txq = iwl_mld_txq_from_mac80211(txq); |
| struct sk_buff *skb = NULL; |
| u8 zero_addr[ETH_ALEN] = {}; |
| |
| /* |
| * No need for threads to be pending here, they can leave the first |
| * taker all the work. |
| * |
| * mld_txq->tx_request logic: |
| * |
| * If 0, no one is currently TXing, set to 1 to indicate current thread |
| * will now start TX and other threads should quit. |
| * |
| * If 1, another thread is currently TXing, set to 2 to indicate to |
| * that thread that there was another request. Since that request may |
| * have raced with the check whether the queue is empty, the TXing |
| * thread should check the queue's status one more time before leaving. |
| * This check is done in order to not leave any TX hanging in the queue |
| * until the next TX invocation (which may not even happen). |
| * |
| * If 2, another thread is currently TXing, and it will already double |
| * check the queue, so do nothing. |
| */ |
| if (atomic_fetch_add_unless(&mld_txq->tx_request, 1, 2)) |
| return; |
| |
| rcu_read_lock(); |
| do { |
| while (likely(!mld_txq->status.stop_full) && |
| (skb = ieee80211_tx_dequeue(mld->hw, txq))) |
| iwl_mld_tx_skb(mld, skb, txq); |
| } while (atomic_dec_return(&mld_txq->tx_request)); |
| |
| IWL_DEBUG_TX(mld, "TXQ of sta %pM tid %d is now empty\n", |
| txq->sta ? txq->sta->addr : zero_addr, txq->tid); |
| |
| rcu_read_unlock(); |
| } |
| |
| static void iwl_mld_hwrate_to_tx_rate(struct iwl_mld *mld, |
| __le32 rate_n_flags_fw, |
| struct ieee80211_tx_info *info) |
| { |
| enum nl80211_band band = info->band; |
| struct ieee80211_tx_rate *tx_rate = &info->status.rates[0]; |
| u32 rate_n_flags = iwl_v3_rate_from_v2_v3(rate_n_flags_fw, |
| mld->fw_rates_ver_3); |
| u32 sgi = rate_n_flags & RATE_MCS_SGI_MSK; |
| u32 chan_width = rate_n_flags & RATE_MCS_CHAN_WIDTH_MSK; |
| u32 format = rate_n_flags & RATE_MCS_MOD_TYPE_MSK; |
| |
| if (sgi) |
| tx_rate->flags |= IEEE80211_TX_RC_SHORT_GI; |
| |
| switch (chan_width) { |
| case RATE_MCS_CHAN_WIDTH_20: |
| break; |
| case RATE_MCS_CHAN_WIDTH_40: |
| tx_rate->flags |= IEEE80211_TX_RC_40_MHZ_WIDTH; |
| break; |
| case RATE_MCS_CHAN_WIDTH_80: |
| tx_rate->flags |= IEEE80211_TX_RC_80_MHZ_WIDTH; |
| break; |
| case RATE_MCS_CHAN_WIDTH_160: |
| tx_rate->flags |= IEEE80211_TX_RC_160_MHZ_WIDTH; |
| break; |
| default: |
| break; |
| } |
| |
| switch (format) { |
| case RATE_MCS_MOD_TYPE_HT: |
| tx_rate->flags |= IEEE80211_TX_RC_MCS; |
| tx_rate->idx = RATE_HT_MCS_INDEX(rate_n_flags); |
| break; |
| case RATE_MCS_MOD_TYPE_VHT: |
| ieee80211_rate_set_vht(tx_rate, |
| rate_n_flags & RATE_MCS_CODE_MSK, |
| u32_get_bits(rate_n_flags, |
| RATE_MCS_NSS_MSK) + 1); |
| tx_rate->flags |= IEEE80211_TX_RC_VHT_MCS; |
| break; |
| case RATE_MCS_MOD_TYPE_HE: |
| case RATE_MCS_MOD_TYPE_EHT: |
| /* mac80211 cannot do this without ieee80211_tx_status_ext() |
| * but it only matters for radiotap |
| */ |
| tx_rate->idx = 0; |
| break; |
| default: |
| tx_rate->idx = |
| iwl_mld_legacy_hw_idx_to_mac80211_idx(rate_n_flags, |
| band); |
| break; |
| } |
| } |
| |
| void iwl_mld_handle_tx_resp_notif(struct iwl_mld *mld, |
| struct iwl_rx_packet *pkt) |
| { |
| struct iwl_tx_resp *tx_resp = (void *)pkt->data; |
| int txq_id = le16_to_cpu(tx_resp->tx_queue); |
| struct agg_tx_status *agg_status = &tx_resp->status; |
| u32 status = le16_to_cpu(agg_status->status); |
| u32 pkt_len = iwl_rx_packet_payload_len(pkt); |
| size_t notif_size = sizeof(*tx_resp) + sizeof(u32); |
| int sta_id = IWL_TX_RES_GET_RA(tx_resp->ra_tid); |
| int tid = IWL_TX_RES_GET_TID(tx_resp->ra_tid); |
| struct ieee80211_link_sta *link_sta; |
| struct iwl_mld_sta *mld_sta; |
| u16 ssn; |
| struct sk_buff_head skbs; |
| u8 skb_freed = 0; |
| bool mgmt = false; |
| bool tx_failure = (status & TX_STATUS_MSK) != TX_STATUS_SUCCESS; |
| |
| if (IWL_FW_CHECK(mld, tx_resp->frame_count != 1, |
| "Invalid tx_resp notif frame_count (%d)\n", |
| tx_resp->frame_count)) |
| return; |
| |
| /* validate the size of the variable part of the notif */ |
| if (IWL_FW_CHECK(mld, notif_size != pkt_len, |
| "Invalid tx_resp notif size (expected=%zu got=%u)\n", |
| notif_size, pkt_len)) |
| return; |
| |
| ssn = le32_to_cpup((__le32 *)agg_status + |
| tx_resp->frame_count) & 0xFFFF; |
| |
| __skb_queue_head_init(&skbs); |
| |
| /* we can free until ssn % q.n_bd not inclusive */ |
| iwl_trans_reclaim(mld->trans, txq_id, ssn, &skbs, false); |
| |
| while (!skb_queue_empty(&skbs)) { |
| struct sk_buff *skb = __skb_dequeue(&skbs); |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| struct ieee80211_hdr *hdr = (void *)skb->data; |
| |
| skb_freed++; |
| |
| iwl_trans_free_tx_cmd(mld->trans, info->driver_data[1]); |
| |
| memset(&info->status, 0, sizeof(info->status)); |
| |
| info->flags &= ~(IEEE80211_TX_STAT_ACK | IEEE80211_TX_STAT_TX_FILTERED); |
| |
| /* inform mac80211 about what happened with the frame */ |
| switch (status & TX_STATUS_MSK) { |
| case TX_STATUS_SUCCESS: |
| case TX_STATUS_DIRECT_DONE: |
| info->flags |= IEEE80211_TX_STAT_ACK; |
| break; |
| default: |
| break; |
| } |
| |
| /* If we are freeing multiple frames, mark all the frames |
| * but the first one as acked, since they were acknowledged |
| * before |
| */ |
| if (skb_freed > 1) |
| info->flags |= IEEE80211_TX_STAT_ACK; |
| |
| if (tx_failure) { |
| enum iwl_fw_ini_time_point tp = |
| IWL_FW_INI_TIME_POINT_TX_FAILED; |
| |
| if (ieee80211_is_action(hdr->frame_control)) |
| tp = IWL_FW_INI_TIME_POINT_TX_WFD_ACTION_FRAME_FAILED; |
| else if (ieee80211_is_mgmt(hdr->frame_control)) |
| mgmt = true; |
| |
| iwl_dbg_tlv_time_point(&mld->fwrt, tp, NULL); |
| } |
| |
| iwl_mld_hwrate_to_tx_rate(mld, tx_resp->initial_rate, info); |
| |
| if (likely(!iwl_mld_time_sync_frame(mld, skb, hdr->addr1))) |
| ieee80211_tx_status_skb(mld->hw, skb); |
| } |
| |
| IWL_DEBUG_TX_REPLY(mld, |
| "TXQ %d status 0x%08x ssn=%d initial_rate 0x%x retries %d\n", |
| txq_id, status, ssn, le32_to_cpu(tx_resp->initial_rate), |
| tx_resp->failure_frame); |
| |
| if (tx_failure && mgmt) |
| iwl_mld_toggle_tx_ant(mld, &mld->mgmt_tx_ant); |
| |
| if (IWL_FW_CHECK(mld, sta_id >= mld->fw->ucode_capa.num_stations, |
| "Got invalid sta_id (%d)\n", sta_id)) |
| return; |
| |
| rcu_read_lock(); |
| |
| link_sta = rcu_dereference(mld->fw_id_to_link_sta[sta_id]); |
| if (!link_sta) { |
| /* This can happen if the TX cmd was sent before pre_rcu_remove |
| * but the TX response was received after |
| */ |
| IWL_DEBUG_TX_REPLY(mld, |
| "Got valid sta_id (%d) but sta is NULL\n", |
| sta_id); |
| goto out; |
| } |
| |
| if (IS_ERR(link_sta)) |
| goto out; |
| |
| mld_sta = iwl_mld_sta_from_mac80211(link_sta->sta); |
| |
| if (tx_failure && mld_sta->sta_state < IEEE80211_STA_AUTHORIZED) |
| iwl_mld_toggle_tx_ant(mld, &mld_sta->data_tx_ant); |
| |
| if (tid < IWL_MAX_TID_COUNT) |
| iwl_mld_count_mpdu_tx(link_sta, 1); |
| |
| out: |
| rcu_read_unlock(); |
| } |
| |
| static void iwl_mld_tx_reclaim_txq(struct iwl_mld *mld, int txq, int index, |
| bool in_flush) |
| { |
| struct sk_buff_head reclaimed_skbs; |
| |
| __skb_queue_head_init(&reclaimed_skbs); |
| |
| iwl_trans_reclaim(mld->trans, txq, index, &reclaimed_skbs, in_flush); |
| |
| while (!skb_queue_empty(&reclaimed_skbs)) { |
| struct sk_buff *skb = __skb_dequeue(&reclaimed_skbs); |
| struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb); |
| |
| iwl_trans_free_tx_cmd(mld->trans, info->driver_data[1]); |
| |
| memset(&info->status, 0, sizeof(info->status)); |
| |
| /* Packet was transmitted successfully, failures come as single |
| * frames because before failing a frame the firmware transmits |
| * it without aggregation at least once. |
| */ |
| if (!in_flush) |
| info->flags |= IEEE80211_TX_STAT_ACK; |
| else |
| info->flags &= ~IEEE80211_TX_STAT_ACK; |
| |
| ieee80211_tx_status_skb(mld->hw, skb); |
| } |
| } |
| |
| int iwl_mld_flush_link_sta_txqs(struct iwl_mld *mld, u32 fw_sta_id) |
| { |
| struct iwl_tx_path_flush_cmd_rsp *rsp; |
| struct iwl_tx_path_flush_cmd flush_cmd = { |
| .sta_id = cpu_to_le32(fw_sta_id), |
| .tid_mask = cpu_to_le16(0xffff), |
| }; |
| struct iwl_host_cmd cmd = { |
| .id = TXPATH_FLUSH, |
| .len = { sizeof(flush_cmd), }, |
| .data = { &flush_cmd, }, |
| .flags = CMD_WANT_SKB, |
| }; |
| int ret, num_flushed_queues; |
| u32 resp_len; |
| |
| IWL_DEBUG_TX_QUEUES(mld, "flush for sta id %d tid mask 0x%x\n", |
| fw_sta_id, 0xffff); |
| |
| ret = iwl_mld_send_cmd(mld, &cmd); |
| if (ret) { |
| IWL_ERR(mld, "Failed to send flush command (%d)\n", ret); |
| return ret; |
| } |
| |
| resp_len = iwl_rx_packet_payload_len(cmd.resp_pkt); |
| if (IWL_FW_CHECK(mld, resp_len != sizeof(*rsp), |
| "Invalid TXPATH_FLUSH response len: %d\n", |
| resp_len)) { |
| ret = -EIO; |
| goto free_rsp; |
| } |
| |
| rsp = (void *)cmd.resp_pkt->data; |
| |
| if (IWL_FW_CHECK(mld, le16_to_cpu(rsp->sta_id) != fw_sta_id, |
| "sta_id %d != rsp_sta_id %d\n", fw_sta_id, |
| le16_to_cpu(rsp->sta_id))) { |
| ret = -EIO; |
| goto free_rsp; |
| } |
| |
| num_flushed_queues = le16_to_cpu(rsp->num_flushed_queues); |
| if (IWL_FW_CHECK(mld, num_flushed_queues > IWL_TX_FLUSH_QUEUE_RSP, |
| "num_flushed_queues %d\n", num_flushed_queues)) { |
| ret = -EIO; |
| goto free_rsp; |
| } |
| |
| for (int i = 0; i < num_flushed_queues; i++) { |
| struct iwl_flush_queue_info *queue_info = &rsp->queues[i]; |
| int read_after = le16_to_cpu(queue_info->read_after_flush); |
| int txq_id = le16_to_cpu(queue_info->queue_num); |
| |
| if (IWL_FW_CHECK(mld, |
| txq_id >= ARRAY_SIZE(mld->fw_id_to_txq), |
| "Invalid txq id %d\n", txq_id)) |
| continue; |
| |
| IWL_DEBUG_TX_QUEUES(mld, |
| "tid %d txq_id %d read-before %d read-after %d\n", |
| le16_to_cpu(queue_info->tid), txq_id, |
| le16_to_cpu(queue_info->read_before_flush), |
| read_after); |
| |
| iwl_mld_tx_reclaim_txq(mld, txq_id, read_after, true); |
| } |
| |
| free_rsp: |
| iwl_free_resp(&cmd); |
| return ret; |
| } |
| |
| int iwl_mld_ensure_queue(struct iwl_mld *mld, struct ieee80211_txq *txq) |
| { |
| struct iwl_mld_txq *mld_txq = iwl_mld_txq_from_mac80211(txq); |
| int ret; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| if (likely(mld_txq->status.allocated)) |
| return 0; |
| |
| ret = iwl_mld_add_txq(mld, txq); |
| |
| spin_lock_bh(&mld->add_txqs_lock); |
| if (!list_empty(&mld_txq->list)) |
| list_del_init(&mld_txq->list); |
| spin_unlock_bh(&mld->add_txqs_lock); |
| |
| return ret; |
| } |
| |
| int iwl_mld_update_sta_txqs(struct iwl_mld *mld, |
| struct ieee80211_sta *sta, |
| u32 old_sta_mask, u32 new_sta_mask) |
| { |
| struct iwl_scd_queue_cfg_cmd cmd = { |
| .operation = cpu_to_le32(IWL_SCD_QUEUE_MODIFY), |
| .u.modify.old_sta_mask = cpu_to_le32(old_sta_mask), |
| .u.modify.new_sta_mask = cpu_to_le32(new_sta_mask), |
| }; |
| |
| lockdep_assert_wiphy(mld->wiphy); |
| |
| for (int tid = 0; tid <= IWL_MAX_TID_COUNT; tid++) { |
| struct ieee80211_txq *txq = |
| sta->txq[tid != IWL_MAX_TID_COUNT ? |
| tid : IEEE80211_NUM_TIDS]; |
| struct iwl_mld_txq *mld_txq = |
| iwl_mld_txq_from_mac80211(txq); |
| int ret; |
| |
| if (!mld_txq->status.allocated) |
| continue; |
| |
| if (tid == IWL_MAX_TID_COUNT) |
| cmd.u.modify.tid = cpu_to_le32(IWL_MGMT_TID); |
| else |
| cmd.u.modify.tid = cpu_to_le32(tid); |
| |
| ret = iwl_mld_send_cmd_pdu(mld, |
| WIDE_ID(DATA_PATH_GROUP, |
| SCD_QUEUE_CONFIG_CMD), |
| &cmd); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| void iwl_mld_handle_compressed_ba_notif(struct iwl_mld *mld, |
| struct iwl_rx_packet *pkt) |
| { |
| struct iwl_compressed_ba_notif *ba_res = (void *)pkt->data; |
| u32 pkt_len = iwl_rx_packet_payload_len(pkt); |
| u16 tfd_cnt = le16_to_cpu(ba_res->tfd_cnt); |
| u8 sta_id = ba_res->sta_id; |
| struct ieee80211_link_sta *link_sta; |
| |
| if (!tfd_cnt) |
| return; |
| |
| if (IWL_FW_CHECK(mld, struct_size(ba_res, tfd, tfd_cnt) > pkt_len, |
| "Short BA notif (tfd_cnt=%d, size:0x%x)\n", |
| tfd_cnt, pkt_len)) |
| return; |
| |
| IWL_DEBUG_TX_REPLY(mld, |
| "BA notif received from sta_id=%d, flags=0x%x, sent:%d, acked:%d\n", |
| sta_id, le32_to_cpu(ba_res->flags), |
| le16_to_cpu(ba_res->txed), |
| le16_to_cpu(ba_res->done)); |
| |
| for (int i = 0; i < tfd_cnt; i++) { |
| struct iwl_compressed_ba_tfd *ba_tfd = &ba_res->tfd[i]; |
| int txq_id = le16_to_cpu(ba_tfd->q_num); |
| int index = le16_to_cpu(ba_tfd->tfd_index); |
| |
| if (IWL_FW_CHECK(mld, |
| txq_id >= ARRAY_SIZE(mld->fw_id_to_txq), |
| "Invalid txq id %d\n", txq_id)) |
| continue; |
| |
| iwl_mld_tx_reclaim_txq(mld, txq_id, index, false); |
| } |
| |
| if (IWL_FW_CHECK(mld, sta_id >= mld->fw->ucode_capa.num_stations, |
| "Got invalid sta_id (%d)\n", sta_id)) |
| return; |
| |
| rcu_read_lock(); |
| |
| link_sta = rcu_dereference(mld->fw_id_to_link_sta[sta_id]); |
| if (IWL_FW_CHECK(mld, IS_ERR_OR_NULL(link_sta), |
| "Got valid sta_id (%d) but link_sta is NULL\n", |
| sta_id)) |
| goto out; |
| |
| iwl_mld_count_mpdu_tx(link_sta, le16_to_cpu(ba_res->txed)); |
| out: |
| rcu_read_unlock(); |
| } |