Index | Thread | Search

From:
Mark Kettenis <mark.kettenis@xs4all.nl>
Subject:
Re: qwz: enable WPA2 association on WCN7850
To:
Marcus Glocker <marcus@nazgul.ch>
Cc:
tech@openbsd.org, kirill@korins.ky, stsp@stsp.name, mail@patrick-wildt.de
Date:
Sun, 26 Apr 2026 13:45:10 +0200

Download raw body.

Thread
> Date: Sat, 25 Apr 2026 23:56:07 +0200
> From: Marcus Glocker <marcus@nazgul.ch>
> 
> Bring the qwz driver up to a working WPA2 client connection on the
> Qualcomm WCN7850 chip.  Tested on the Samsung Galaxy Book4 Edge.
> 
> Major changes:
> 
> 1. Fix the RX path.
>    Wire up the WCN7850 descriptor accesses that were unset; override
>    the descriptor size to match what the FW actually writes (512 bytes
>    instead of struct sizeof 472); add the first-line filters that drop
>    FW-injected garbage frames before net80211 mistakes them for fake
>    auth/deauth.
> 
> 2. Fix the TX path.
>    Port Linux's WiFi7 "TX bank" infrastructure: a per-VDEV register
>    that holds encap/encrypt/search settings the descriptor used to
>    carry inline.  Rewrite the TX descriptor builder for the WiFi7 wire
>    format.  Fix an encrypt_type default that was making the FW try to
>    WEP-encrypt plain-text EAPOL frames.
> 
> 3. Fix MSI interrupt routing.
>    Correct the DP IRQ group's MSI vector calculation, and free the
>    vector DP group 0 needs (was being held by an unused pktlog
>    interrupt).  Without these, RX completions never fired regardless
>    of how correct the rest of the path was.

That code still looks a bit dodgy to me.  The equivalent code in
qwx(4) looks a bit dodgy too though.  I'll have a look over there
first to see if I can make it a bit less dodgy.  No reason not to move
forward with qwz(4).

> 4. Make the WPA2 4-way handshake complete.
>    Move WMI_PEER_AUTHORIZE to fire after key install, not before; the
>    old order told the FW crypto was up while plain-text EAPOL was still
>    in flight, crashing the FW.  Mask the AID to its 14-bit value before
>    handing it to the FW.  Add the missing REO queue setup for non-QoS
>    frames, which is where EAPOL lives.
> 
> 5. Add non-coherent DMA cache sync on RX and TX.
>    Without explicit flushes the CPU and FW see different bytes for
>    the same buffer.  This was the root cause of "garbage RX frames":
>    they were always real EAPOL Msg 1 frames torn by stale CPU cache
>    lines.

DMA on the galaxybook should be coherent.  But bus_dmamap_sync() still
issues a barrier instruction which might be needed to make sure reads
by the CPU aren't issued in the wrong order.  So those extra
bus_dmamap_sync() calls are needed.

> 
> 6. Update register/descriptor defines from ath11k to ath12k WiFi7.
>    The TX descriptor wire format changed completely between
>    generations: bit positions, field set, even the number of 32-bit
>    words.  Partial updates wouldn't have worked.
> 
> 7. Cleanup.
>    Remove some debug printfs and the diagnostic counters added during
>    the bring-up to verify the path was working.
> 
> Known limitations:
> 
>   - Firmware occasionally crashes after sustained traffic; driver
>     recovers via the existing RDDM path in if_qwz_pci.c without a
>     system reboot.  Root-causing this is the next follow-up.
>   - One PN-replay loop in qwz_dp_peer_rx_pn_replay_config doesn't
>     iterate the non-QoS TID slot.  Cosmetic for normal use; will
>     land as a separate small commit.
> 
> Further testing, feedback, OKs, welcome.

Doesn't seem to get me much further on the vivobook.  Tried it first
with a FritzBox.  After an "ifconfig qwz0 up" I see these messages:

qwz_pull_reg_chan_list_ext_update_ev: not implemented
qwz0: failed to extract regulatory info from received event
qwz_pull_reg_chan_list_ext_update_ev: not implemented
qwz0: failed to extract regulatory info from received event

This isn't different from before.  I suppose this is something we can
ignore for now.

After dong an "ifconfig qwz0 nwid xxx wpakey yyy" I get a few more of
these and then:

qwz0: fatal firmware error
qwz0: fatal firmware error
qwz0: fatal firmware error

I think that is where it resets and tries again, where it becomes:

qwz0: fatal firmware error
qwz0: fatal firmware error
qwz0: failed to send wlan mode request, err = 1
qwz0: qmi failed to send wlan mode off: 1

And after a few more resets it becomes:

qwz0: tx credits timeout
qwz0: failed to send WMI_PDEV_SET_PARAM cmd
qwz0: failed to enable MESH MCAST ENABLE for pdev 0: 35

After that the device appears to be dead.  An attempt to revive with
"ifconfig qwz0 down; ifconfig qwz0 up" results in:

ifconfig: qwz0: SIOCSIFFLAGS: Resource temporarily unavailable

and more:

qwz0: tx credits timeout
qwz0: failed to send WMI_PDEV_SET_PARAM cmd
qwz0: failed to enable MESH MCAST ENABLE for pdev 0: 35

I did see it reach the "status: active" state at some point, but that
didn't last for more than a few seconds.

I also tried with my athn(4) OpenBSD access point.  There it does
reach the "status: active" state and stays in that state.  If I then
do "ifconfig qwz0 autoconf", I get a few firmware errors:

qwz0: fatal firmware error
qwz0: fatal firmware error
qwz0: fatal firmware error

But it recovers and remains associated.  After a while I also see:

qwz0: fatal firmware error
qwz0: fatal firmware error
qwz0: fatal firmware error
qwz0: peer delete unmap timeout
qwz0: unable to delete BSS peer: 35
qwz0: failed to send wlan mode request, err = 1
qwz0: qmi failed to send wlan mode off: 1

but it stays associated.  My DHCP server sees the DHCP request and
offers a lease, but the offered address never gets configured.

So a little bit of progress.  I don't see a reason not to commit this.

> Index: sys/dev/ic/qwz.c
> ===================================================================
> RCS file: /cvs/src/sys/dev/ic/qwz.c,v
> diff -u -p -u -p -r1.25 qwz.c
> --- sys/dev/ic/qwz.c	21 Apr 2026 08:56:22 -0000	1.25
> +++ sys/dev/ic/qwz.c	25 Apr 2026 21:29:57 -0000
> @@ -730,8 +730,17 @@ qwz_add_sta_key(struct qwz_softc *sc, st
>  		nq->flags |= QWZ_NODE_FLAG_HAVE_PAIRWISE_KEY;
>  
>  	if ((nq->flags & want_keymask) == want_keymask) {
> -		DPRINTF("marking port %s valid\n",
> -		    ether_sprintf(ni->ni_macaddr));
> +		uint8_t pdev_id = 0; /* TODO: derive pdev ID somehow? */
> +
> +		/* 4-way handshake done; authorize the BSS peer now. */
> +		ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr, arvif->vdev_id,
> +		    pdev_id, WMI_PEER_AUTHORIZE, 1);
> +		if (ret) {
> +			printf("%s: unable to authorize BSS peer: %d\n",
> +			    sc->sc_dev.dv_xname, ret);
> +			return ret;
> +		}
> +
>  		ni->ni_port_valid = 1;
>  		ieee80211_set_link_state(ic, LINK_STATE_UP);
>  	}
> @@ -1116,7 +1125,7 @@ qwz_hw_wcn7850_rx_desc_get_mpdu_seq_ctl_
>  	      le32toh(desc->u.wcn7850.mpdu_start.info4));
>  }
>  
> -int
> +bool
>  qwz_hw_wcn7850_rx_desc_get_mpdu_fc_valid(struct hal_rx_desc *desc)
>  {
>  	return !!FIELD_GET(RX_MPDU_START_INFO4_MPDU_FCTRL_VALID,
> @@ -1230,6 +1239,13 @@ qwz_hw_wcn7850_rx_desc_is_da_mcbc(struct
>  	    le32toh(desc->u.wcn7850.msdu_end.info13));
>  }
>  
> +bool
> +qwz_hw_wcn7850_dp_rx_h_msdu_done(struct hal_rx_desc *desc)
> +{
> +	return !!(le32toh(desc->u.wcn7850.msdu_end.info14) &
> +	    RX_MSDU_END_INFO14_MSDU_DONE);
> +}
> +
>  int
>  qwz_hw_wcn7850_dp_rx_h_is_decrypted(struct hal_rx_desc *desc)
>  {
> @@ -1270,7 +1286,21 @@ qwz_hw_wcn7850_dp_rx_h_mpdu_err(struct h
>  
>  uint32_t qwz_hw_wcn7850_get_rx_desc_size(void)
>  {
> -	return sizeof(struct hal_rx_desc_wcn7850);
> +	/*
> +	 * Empirically observed on WCN7850 hw2.0 fw 0x110cffff: the FW
> +	 * places the MSDU payload at offset 512 of the buffer (with the
> +	 * mpdu_start_tag at 216 and mpdu_start data at 224, matching our
> +	 * 80-byte rx_padding0). The struct sizeof works out to only 472
> +	 * bytes, so override the descriptor size getter to return the
> +	 * actual 512 bytes for m_adj to strip the right amount.
> +	 *
> +	 * NOTE: keeping struct sizeof at 472 is intentional; bumping
> +	 * pkt_hdr_tlv to 168 to make sizeof = 512 caused spontaneous
> +	 * machine reboots, suggesting a downstream code path (likely the
> +	 * EAPOL TX response) was crashing the FW once real frames started
> +	 * arriving. We isolate that here by only changing what m_adj sees.
> +	 */
> +	return 512;
>  }
>  
>  uint8_t
> @@ -1583,9 +1613,9 @@ const struct ce_attr qwz_host_ce_config_
>  		.dest_nentries = 0,
>  	},
>  
> -	/* CE5: target->host pktlog */
> +	/* CE5: target->host pktlog (DIS_INTR: frees MSI vector 8 for DP group 0) */
>  	{
> -		.flags = CE_ATTR_FLAGS,
> +		.flags = CE_ATTR_FLAGS | CE_ATTR_DIS_INTR,
>  		.src_nentries = 0,
>  		.src_sz_max = 0,
>  		.dest_nentries = 0,
> @@ -1652,9 +1682,9 @@ const struct hal_rx_ops hal_rx_wcn7850_o
>  #ifdef notyet
>  	.rx_desc_get_mesh_ctl = qwz_hw_wcn7850_rx_desc_get_mesh_ctl,
>  	.rx_desc_get_mpdu_seq_ctl_vld = qwz_hw_wcn7850_rx_desc_get_mpdu_seq_ctl_vld,
> -	.rx_desc_get_mpdu_fc_valid = qwz_hw_wcn7850_rx_desc_get_mpdu_fc_valid,
>  	.rx_desc_get_mpdu_start_seq_no = qwz_hw_wcn7850_rx_desc_get_mpdu_start_seq_no,
>  #endif
> +	.rx_desc_get_mpdu_fc_valid = qwz_hw_wcn7850_rx_desc_get_mpdu_fc_valid,
>  	.rx_desc_get_msdu_len = qwz_hw_wcn7850_rx_desc_get_msdu_len,
>  #ifdef notyet
>  	.rx_desc_get_msdu_sgi = qwz_hw_wcn7850_rx_desc_get_msdu_sgi,
> @@ -1682,10 +1712,10 @@ const struct hal_rx_ops hal_rx_wcn7850_o
>  	.rx_desc_get_dot11_hdr = qwz_hw_wcn7850_rx_desc_get_dot11_hdr,
>  	.rx_desc_get_crypto_header = qwz_hw_wcn7850_rx_desc_get_crypto_hdr,
>  	.rx_desc_get_mpdu_frame_ctl = qwz_hw_wcn7850_rx_desc_get_mpdu_frame_ctl,
> -	.dp_rx_h_msdu_done = qwz_hw_wcn7850_dp_rx_h_msdu_done,
>  	.dp_rx_h_l4_cksum_fail = qwz_hw_wcn7850_dp_rx_h_l4_cksum_fail,
>  	.dp_rx_h_ip_cksum_fail = qwz_hw_wcn7850_dp_rx_h_ip_cksum_fail,
>  #endif
> +	.dp_rx_h_msdu_done = qwz_hw_wcn7850_dp_rx_h_msdu_done,
>  	.dp_rx_h_is_decrypted = qwz_hw_wcn7850_dp_rx_h_is_decrypted,
>  	.dp_rx_h_mpdu_err = qwz_hw_wcn7850_dp_rx_h_mpdu_err,
>  	.rx_desc_get_desc_size = qwz_hw_wcn7850_get_rx_desc_size,
> @@ -7613,29 +7643,6 @@ qwz_hal_srng_get_params(struct qwz_softc
>  	params->flags = srng->flags;
>  }
>  
> -void
> -qwz_hal_tx_init_data_ring(struct qwz_softc *sc, struct hal_srng *srng)
> -{
> -	struct hal_srng_params params;
> -	struct hal_tlv_hdr *tlv;
> -	int i, entry_size;
> -	uint8_t *desc;
> -
> -	memset(&params, 0, sizeof(params));
> -
> -	entry_size = qwz_hal_srng_get_entrysize(sc, HAL_TCL_DATA);
> -	qwz_hal_srng_get_params(sc, srng, &params);
> -	desc = (uint8_t *)params.ring_base_vaddr;
> -
> -	for (i = 0; i < params.num_entries; i++) {
> -		tlv = (struct hal_tlv_hdr *)desc;
> -		tlv->tl = FIELD_PREP(HAL_TLV_HDR_TAG, HAL_TCL_DATA_CMD) |
> -		    FIELD_PREP(HAL_TLV_HDR_LEN,
> -		    sizeof(struct hal_tcl_data_cmd));
> -		desc += entry_size;
> -	}
> -}
> -
>  #define DSCP_TID_MAP_TBL_ENTRY_SIZE 64
>  
>  /* dscp_tid_map - Default DSCP-TID mapping
> @@ -8021,9 +8028,6 @@ qwz_dp_srng_common_setup(struct qwz_soft
>  			    sc->sc_dev.dv_xname, i, ret);
>  			goto err;
>  		}
> -
> -		srng = &sc->hal.srng_list[dp->tx_ring[i].tcl_data_ring.ring_id];
> -		qwz_hal_tx_init_data_ring(sc, srng);
>  	}
>  
>  	ret = qwz_dp_srng_setup(sc, &dp->reo_reinject_ring, HAL_REO_REINJECT,
> @@ -8241,7 +8245,29 @@ void *ath12k_dp_cc_get_desc_addr_ptr(str
>  {
>  	struct qwz_dp *dp = &sc->dp;
>  
> -	return QWZ_DMA_KVA(dp->spt_info[ppt_idx].mem) + spt_idx;
> +	/*
> +	 * Cast to void** before adding spt_idx so arithmetic steps by
> +	 * sizeof(void*), not by 1 byte.
> +	 */
> +	return (void **)QWZ_DMA_KVA(dp->spt_info[ppt_idx].mem) + spt_idx;
> +}
> +
> +struct ath12k_rx_desc_info *
> +qwz_dp_get_rx_desc(struct qwz_softc *sc, uint32_t cookie)
> +{
> +	struct ath12k_rx_desc_info **desc_addr_ptr;
> +	uint16_t ppt_idx, spt_idx;
> +
> +	ppt_idx = FIELD_GET(ATH12K_DP_CC_COOKIE_PPT, cookie);
> +	spt_idx = FIELD_GET(ATH12K_DP_CC_COOKIE_SPT, cookie);
> +
> +	if (ppt_idx < ATH12K_RX_SPT_PAGE_OFFSET ||
> +	    ppt_idx >= ATH12K_RX_SPT_PAGE_OFFSET + ATH12K_NUM_RX_SPT_PAGES ||
> +	    spt_idx >= ATH12K_MAX_SPT_ENTRIES)
> +		return NULL;
> +
> +	desc_addr_ptr = ath12k_dp_cc_get_desc_addr_ptr(sc, ppt_idx, spt_idx);
> +	return *desc_addr_ptr;
>  }
>  
>  int
> @@ -10042,10 +10068,6 @@ qwz_peer_assoc_conf_event(struct qwz_sof
>  		return;
>  	}
>  
> -	DNPRINTF(QWZ_D_WMI, "%s: event peer assoc conf ev vdev id %d "
> -	    "macaddr %s\n", __func__, peer_assoc_conf.vdev_id,
> -	    ether_sprintf((u_char *)peer_assoc_conf.macaddr));
> -
>  	sc->peer_assoc_done = 1;
>  	wakeup(&sc->peer_assoc_done);
>  }
> @@ -12497,8 +12519,11 @@ qwz_peer_map_event(struct qwz_softc *sc,
>  	spin_lock_bh(&ab->base_lock);
>  #endif
>  	ni = ieee80211_find_node(ic, mac_addr);
> -	if (ni == NULL)
> +	if (ni == NULL) {
> +		printf("%s: peer_map: no node for %s\n", sc->sc_dev.dv_xname,
> +		    ether_sprintf(mac_addr));
>  		return;
> +	}
>  	nq = (struct qwz_node *)ni;
>  	peer = &nq->peer;
>  
> @@ -12639,6 +12664,9 @@ qwz_dp_htt_htc_t2h_msg_handler(struct qw
>  		ath12k_htt_backpressure_event_handler(ab, skb);
>  		break;
>  #endif
> +	case HTT_T2H_MSG_TYPE_PRIMARY_LINK_PEER_MIGRATE_IND:
> +		/* MLO peer migration; no action needed for non-MLO ops. */
> +		break;
>  	default:
>  		printf("%s: htt event %d not handled\n", __func__, type);
>  		break;
> @@ -13040,6 +13068,13 @@ qwz_dp_rxbufs_replenish(struct qwz_softc
>  			goto fail_free_mbuf;
>  		}
>  
> +		/*
> +		 * Invalidate any stale cache lines covering this buffer
> +		 * before the FW writes RX data into it.
> +		 */
> +		bus_dmamap_sync(sc->sc_dmat, rx_desc->map, 0,
> +		    rx_desc->map->dm_mapsize, BUS_DMASYNC_PREREAD);
> +
>  		cookie = rx_desc->cookie;
>  
>  		desc = qwz_hal_srng_src_get_next_entry(sc, srng);
> @@ -13049,6 +13084,7 @@ qwz_dp_rxbufs_replenish(struct qwz_softc
>  		TAILQ_REMOVE(used_list, rx_desc, entry);
>  
>  		rx_desc->m = m;
> +		rx_desc->in_use = 1;
>  		m = NULL;
>  
>  		num_remain--;
> @@ -13636,6 +13672,97 @@ qwz_dp_update_vdev_search(struct qwz_sof
>  	}
>  }
>  
> +/*
> + * Compute the 32-bit TX bank config for this VDEV. Mirror of Linux
> + * ath12k_wifi7_dp_tx_get_vdev_bank_config().
> + */
> +uint32_t
> +qwz_dp_tx_get_vdev_bank_config(struct qwz_softc *sc, struct qwz_vif *arvif)
> +{
> +	uint32_t bank_config = 0;
> +	enum hal_tcl_encap_type encap_type;
> +
> +	if (test_bit(ATH12K_FLAG_RAW_MODE, sc->sc_flags))
> +		encap_type = HAL_TCL_ENCAP_TYPE_RAW;
> +	else
> +		encap_type = HAL_TCL_ENCAP_TYPE_NATIVE_WIFI;
> +
> +	bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_ENCAP_TYPE, encap_type);
> +	bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_SRC_BUFFER_SWAP, 0) |
> +	    FIELD_PREP(HAL_TX_BANK_CONFIG_LINK_META_SWAP, 0) |
> +	    FIELD_PREP(HAL_TX_BANK_CONFIG_EPD, 0);
> +
> +	if (arvif->vdev_type == WMI_VDEV_TYPE_STA)
> +		bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_INDEX_LOOKUP_EN, 1);
> +
> +	bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_ADDRX_EN,
> +	    !!(arvif->hal_addr_search_flags & HAL_TX_ADDRX_EN)) |
> +	    FIELD_PREP(HAL_TX_BANK_CONFIG_ADDRY_EN,
> +	    !!(arvif->hal_addr_search_flags & HAL_TX_ADDRY_EN));
> +
> +	bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_MESH_EN, 0) |
> +	    FIELD_PREP(HAL_TX_BANK_CONFIG_VDEV_ID_CHECK_EN, 1);
> +	bank_config |= FIELD_PREP(HAL_TX_BANK_CONFIG_DSCP_TIP_MAP_ID, 0);
> +
> +	return bank_config;
> +}
> +
> +/*
> + * Write the per-bank TX config register so the FW picks up the encap/
> + * encrypt/search settings when it processes a tcl_data_cmd with this
> + * bank_id. Mirror of Linux ath12k_wifi7_hal_tx_configure_bank_register().
> + */
> +void
> +qwz_hal_tx_configure_bank_register(struct qwz_softc *sc, uint32_t bank_config,
> +    uint8_t bank_id)
> +{
> +	sc->ops.write32(sc, HAL_TCL_SW_CONFIG_BANK_ADDR + 4 * bank_id,
> +	    bank_config);
> +}
> +
> +/*
> + * Allocate (or reuse) a TX bank profile that matches the requested
> + * bank_config. Configures the FW register on first use of a bank.
> + * Returns -1 if no bank slot is available.
> + */
> +int
> +qwz_dp_tx_get_bank_profile(struct qwz_softc *sc, struct qwz_vif *arvif)
> +{
> +	struct qwz_dp *dp = &sc->dp;
> +	uint32_t bank_config = qwz_dp_tx_get_vdev_bank_config(sc, arvif);
> +	int i, bank_id = -1, configure_register = 0;
> +
> +	for (i = 0; i < dp->num_bank_profiles; i++) {
> +		if (dp->bank_profiles[i].is_configured &&
> +		    dp->bank_profiles[i].bank_config == bank_config) {
> +			bank_id = i;
> +			goto inc_ref_and_return;
> +		}
> +		if (!dp->bank_profiles[i].is_configured ||
> +		    dp->bank_profiles[i].num_users == 0) {
> +			bank_id = i;
> +			goto configure_and_return;
> +		}
> +	}
> +
> +	if (bank_id == -1) {
> +		printf("%s: out of TX bank slots\n", sc->sc_dev.dv_xname);
> +		return -1;
> +	}
> +
> +configure_and_return:
> +	dp->bank_profiles[bank_id].is_configured = 1;
> +	dp->bank_profiles[bank_id].bank_config = bank_config;
> +	configure_register = 1;
> +inc_ref_and_return:
> +	dp->bank_profiles[bank_id].num_users++;
> +
> +	if (configure_register)
> +		qwz_hal_tx_configure_bank_register(sc, bank_config, bank_id);
> +
> +	return bank_id;
> +}
> +
>  void
>  qwz_dp_vdev_tx_attach(struct qwz_softc *sc, struct qwz_pdev *pdev,
>      struct qwz_vif *arvif)
> @@ -13648,6 +13775,15 @@ qwz_dp_vdev_tx_attach(struct qwz_softc *
>  	arvif->tcl_metadata &= ~HTT_TCL_META_DATA_VALID_HTT;
>  
>  	qwz_dp_update_vdev_search(sc, arvif);
> +
> +	/*
> +	 * Allocate and configure a TX bank for this VDEV. The bank
> +	 * register holds encap_type, encrypt_type, addrx/y_en, etc., and
> +	 * the per-frame tcl_data_cmd descriptor only references it by
> +	 * bank_id. WCN7850 (WiFi7) requires this; the older inline
> +	 * encoding crashes the FW.
> +	 */
> +	arvif->bank_id = qwz_dp_tx_get_bank_profile(sc, arvif);
>  }
>  
>  void
> @@ -13838,6 +13974,8 @@ qwz_dp_tx_complete_msdu(struct qwz_softc
>  		return;
>  	}
>  
> +	bus_dmamap_sync(sc->sc_dmat, tx_data->map, 0,
> +	    tx_data->map->dm_mapsize, BUS_DMASYNC_POSTWRITE);
>  	bus_dmamap_unload(sc->sc_dmat, tx_data->map);
>  	m_freem(tx_data->m);
>  	tx_data->m = NULL;
> @@ -14096,7 +14234,7 @@ qwz_dp_process_rx_err_buf(struct qwz_sof
>  	struct qwz_rx_data *rx_data;
>  	struct hal_rx_desc *rx_desc;
>  	uint16_t msdu_len;
> -	uint32_t hal_rx_desc_sz = sc->hw_params.hal_desc_sz;
> +	uint32_t hal_rx_desc_sz = sc->hal.hal_desc_sz;
>  
>  	if (buf_id >= rx_ring->bufs_max || isset(rx_ring->freemap, buf_id))
>  		return;
> @@ -14244,7 +14382,6 @@ qwz_dp_process_rx_err(struct qwz_softc *
>  
>  	return tot_n_bufs_reaped;
>  #endif
> -	printf("%s:%d\n", __func__, __LINE__);
>  	return 0;
>  }
>  
> @@ -14493,7 +14630,6 @@ done:
>  
>  	return total_num_buffs_reaped;
>  #endif
> -	printf("%s:%d\n", __func__, __LINE__);
>  	return 0;
>  }
>  
> @@ -14659,11 +14795,6 @@ qwz_dp_rx_h_undecap_raw(struct qwz_softc
>  #endif
>  }
>  
> -static inline uint8_t *
> -qwz_dp_rx_h_80211_hdr(struct qwz_softc *sc, struct hal_rx_desc *desc)
> -{
> -	return sc->hal_rx_ops->rx_desc_get_hdr_status(desc);
> -}
>  
>  static inline enum hal_encrypt_type
>  qwz_dp_rx_h_enctype(struct qwz_softc *sc, struct hal_rx_desc *desc)
> @@ -14681,46 +14812,95 @@ qwz_dp_rx_h_msdu_start_decap_type(struct
>  }
>  
>  void
> +qwz_dp_rx_h_undecap_eth(struct qwz_softc *sc, struct qwz_rx_msdu *msdu,
> +    struct hal_rx_desc *rx_desc)
> +{
> +	struct rx_mpdu_start_qcn9274 *mpdu = &rx_desc->u.wcn7850.mpdu_start;
> +	struct ether_header *eth;
> +	uint8_t da[IEEE80211_ADDR_LEN], sa[IEEE80211_ADDR_LEN];
> +	uint8_t llc[8] = { 0xaa, 0xaa, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 };
> +	struct ieee80211_frame tmpwh;
> +	int hdrlen;
> +	uint8_t *p;
> +	struct mbuf *m = msdu->m;
> +
> +	if (m->m_pkthdr.len < ETHER_HDR_LEN)
> +		return;
> +
> +	eth = mtod(m, struct ether_header *);
> +	memcpy(da, eth->ether_dhost, IEEE80211_ADDR_LEN);
> +	memcpy(sa, eth->ether_shost, IEEE80211_ADDR_LEN);
> +	memcpy(&llc[6], &eth->ether_type, 2); /* SNAP type = ethertype (BE) */
> +
> +	/* Determine 802.11 header length from mpdu_start frame_ctrl. */
> +	tmpwh.i_fc[0] = le16toh(mpdu->frame_ctrl) & 0xff;
> +	tmpwh.i_fc[1] = (le16toh(mpdu->frame_ctrl) >> 8) & 0xff;
> +	hdrlen = sizeof(struct ieee80211_frame);
> +	if (ieee80211_has_addr4(&tmpwh))
> +		hdrlen += IEEE80211_ADDR_LEN;
> +	if (ieee80211_has_qos(&tmpwh))
> +		hdrlen += 2;
> +
> +	/* Strip Ethernet header. */
> +	m_adj(m, ETHER_HDR_LEN);
> +
> +	/* Prepend LLC/SNAP header. */
> +	M_PREPEND(m, 8, M_DONTWAIT);
> +	if (m == NULL) {
> +		msdu->m = NULL;
> +		return;
> +	}
> +	msdu->m = m;
> +	memcpy(mtod(m, void *), llc, 8);
> +
> +	/* Prepend 802.11 header. */
> +	M_PREPEND(m, hdrlen, M_DONTWAIT);
> +	if (m == NULL) {
> +		msdu->m = NULL;
> +		return;
> +	}
> +	msdu->m = m;
> +	p = mtod(m, uint8_t *);
> +	memset(p, 0, hdrlen);
> +	p[0] = tmpwh.i_fc[0];
> +	p[1] = tmpwh.i_fc[1];
> +	memcpy(p + 2, &mpdu->duration, 2);
> +	memcpy(p + 4, mpdu->addr1, IEEE80211_ADDR_LEN);
> +	memcpy(p + 10, mpdu->addr2, IEEE80211_ADDR_LEN);
> +	memcpy(p + 16, mpdu->addr3, IEEE80211_ADDR_LEN);
> +	memcpy(p + 22, &mpdu->seq_ctrl, 2);
> +	if (ieee80211_has_addr4(&tmpwh)) {
> +		memcpy(p + 24, mpdu->addr4, IEEE80211_ADDR_LEN);
> +		if (ieee80211_has_qos(&tmpwh))
> +			memcpy(p + 30, &mpdu->qos_ctrl, 2);
> +	} else if (ieee80211_has_qos(&tmpwh)) {
> +		memcpy(p + 24, &mpdu->qos_ctrl, 2);
> +	}
> +
> +	/* Override addr1/addr3 with actual DA/SA from Ethernet header. */
> +	memcpy(p + 4, da, IEEE80211_ADDR_LEN);
> +	memcpy(p + 16, sa, IEEE80211_ADDR_LEN);
> +}
> +
> +void
>  qwz_dp_rx_h_undecap(struct qwz_softc *sc, struct qwz_rx_msdu *msdu,
>      struct hal_rx_desc *rx_desc, enum hal_encrypt_type enctype,
>      int decrypted)
>  {
> -	uint8_t *first_hdr;
>  	uint8_t decap;
>  
> -	first_hdr = qwz_dp_rx_h_80211_hdr(sc, rx_desc);
>  	decap = qwz_dp_rx_h_msdu_start_decap_type(sc, rx_desc);
>  
>  	switch (decap) {
>  	case DP_RX_DECAP_TYPE_NATIVE_WIFI:
> -		qwz_dp_rx_h_undecap_nwifi(sc, msdu, first_hdr, enctype);
> +		qwz_dp_rx_h_undecap_nwifi(sc, msdu, NULL, enctype);
>  		break;
>  	case DP_RX_DECAP_TYPE_RAW:
>  		qwz_dp_rx_h_undecap_raw(sc, msdu, enctype, decrypted);
>  		break;
> -#if 0
>  	case DP_RX_DECAP_TYPE_ETHERNET2_DIX:
> -		ehdr = (struct ethhdr *)msdu->data;
> -
> -		/* mac80211 allows fast path only for authorized STA */
> -		if (ehdr->h_proto == cpu_to_be16(ETH_P_PAE)) {
> -			ATH12K_SKB_RXCB(msdu)->is_eapol = true;
> -			ath12k_dp_rx_h_undecap_eth(ar, msdu, first_hdr,
> -						   enctype, status);
> -			break;
> -		}
> -
> -		/* PN for mcast packets will be validated in mac80211;
> -		 * remove eth header and add 802.11 header.
> -		 */
> -		if (ATH12K_SKB_RXCB(msdu)->is_mcbc && decrypted)
> -			ath12k_dp_rx_h_undecap_eth(ar, msdu, first_hdr,
> -						   enctype, status);
> -		break;
> -	case DP_RX_DECAP_TYPE_8023:
> -		/* TODO: Handle undecap for these formats */
> +		qwz_dp_rx_h_undecap_eth(sc, msdu, rx_desc);
>  		break;
> -#endif
>  	}
>  }
>  
> @@ -14828,7 +15008,7 @@ qwz_dp_rx_process_msdu(struct qwz_softc 
>  	uint8_t l3_pad_bytes;
>  	uint16_t msdu_len;
>  	int ret;
> -	uint32_t hal_rx_desc_sz = sc->hw_params.hal_desc_sz;
> +	uint32_t hal_rx_desc_sz = sc->hal.hal_desc_sz;
>  
>  	last_buf = qwz_dp_rx_get_msdu_last_buf(msdu_list, msdu);
>  	if (!last_buf) {
> @@ -14845,6 +15025,25 @@ qwz_dp_rx_process_msdu(struct qwz_softc 
>  		return EIO;
>  	}
>  
> +	/* Drop non-802.11 frames (e.g. WCN7850 FW internal messages). */
> +	if (!sc->hal_rx_ops->rx_desc_get_mpdu_fc_valid(rx_desc))
> +		return EIO;
> +
> +	/*
> +	 * WCN7850 FW injects internal messages into the REO ring with
> +	 * fc_valid=1 but garbage 802.11 contents. Their synthetic addr1
> +	 * always ends in 84:e1 (regardless of the multicast bit). Drop
> +	 * those, then drop any remaining unicast frames not addressed
> +	 * to our own MAC.
> +	 */
> +	struct rx_mpdu_start_qcn9274 *mpdu = &rx_desc->u.wcn7850.mpdu_start;
> +
> +	if (mpdu->addr1[4] == 0x84 && mpdu->addr1[5] == 0xe1)
> +		return EIO;
> +	if (!(mpdu->addr1[0] & 0x01) &&
> +	    !IEEE80211_ADDR_EQ(mpdu->addr1, sc->sc_ic.ic_myaddr))
> +		return EIO;
> +
>  	msdu->rx_desc = rx_desc;
>  	msdu_len = qwz_dp_rx_h_msdu_start_msdu_len(sc, rx_desc);
>  	l3_pad_bytes = qwz_dp_rx_h_msdu_end_l3pad(sc, lrx_desc);
> @@ -14935,27 +15134,25 @@ qwz_dp_rx_process_received_packets(struc
>  int
>  qwz_dp_process_rx(struct qwz_softc *sc, int ring_id)
>  {
> -#if 0
>  	struct qwz_dp *dp = &sc->dp;
> -	struct qwz_pdev_dp *pdev_dp = &sc->pdev_dp;
> -	struct dp_rxdma_ring *rx_ring;
>  	int num_buffs_reaped[MAX_RADIOS] = {0};
>  	struct qwz_rx_msdu_list msdu_list[MAX_RADIOS];
> +	TAILQ_HEAD(, ath12k_rx_desc_info) rx_desc_used_list;
> +	struct ath12k_rx_desc_info *desc_info;
>  	struct qwz_rx_msdu *msdu;
>  	struct mbuf *m;
> -	struct qwz_rx_data *rx_data;
>  	int total_msdu_reaped = 0;
>  	struct hal_srng *srng;
>  	int done = 0;
> -	int idx;
> +	int budget = 512;
>  	unsigned int mac_id;
>  	struct hal_reo_dest_ring *desc;
>  	enum hal_reo_dest_ring_push_reason push_reason;
> -	uint32_t cookie;
>  	int i;
>  
>  	for (i = 0; i < MAX_RADIOS; i++)
>  		TAILQ_INIT(&msdu_list[i]);
> +	TAILQ_INIT(&rx_desc_used_list);
>  
>  	srng = &sc->hal.srng_list[dp->reo_dst_ring[ring_id].ring_id];
>  #ifdef notyet
> @@ -14964,41 +15161,60 @@ qwz_dp_process_rx(struct qwz_softc *sc, 
>  try_again:
>  	qwz_hal_srng_access_begin(sc, srng);
>  
> -	while ((desc = (struct hal_reo_dest_ring *)
> +	while (budget > 0 && (desc = (struct hal_reo_dest_ring *)
>  	    qwz_hal_srng_dst_get_next_entry(sc, srng))) {
> +		budget--;
> +		uint64_t desc_va;
> +		uint32_t cookie;
> +
>  		cookie = FIELD_GET(BUFFER_ADDR_INFO1_SW_COOKIE,
>  		    desc->buf_addr_info.info1);
> -		idx = FIELD_GET(DP_RXDMA_BUF_COOKIE_BUF_ID, cookie);
> -		mac_id = FIELD_GET(DP_RXDMA_BUF_COOKIE_PDEV_ID, cookie);
> +		desc_va = (uint64_t)desc->buf_va_hi << 32 | desc->buf_va_lo;
>  
> -		if (mac_id >= MAX_RADIOS)
> -			continue;
> +		/* Validate VA looks like a kernel address before dereferencing */
> +		if (desc_va >= 0xffff800000000000ULL)
> +			desc_info = (struct ath12k_rx_desc_info *)desc_va;
> +		else
> +			desc_info = NULL;
>  
> -		rx_ring = &pdev_dp->rx_refill_buf_ring;
> -		if (idx >= rx_ring->bufs_max || isset(rx_ring->freemap, idx))
> +		/* Fall back to cookie-based lookup if HW CC gave invalid VA */
> +		if (desc_info == NULL ||
> +		    desc_info->magic != ATH12K_DP_RX_DESC_MAGIC) {
> +			desc_info = qwz_dp_get_rx_desc(sc, cookie);
> +			if (desc_info == NULL ||
> +			    desc_info->magic != ATH12K_DP_RX_DESC_MAGIC)
> +				continue;
> +		}
> +
> +		if (!desc_info->in_use)
>  			continue;
>  
> -		rx_data = &rx_ring->rx_data[idx];
> -		bus_dmamap_unload(sc->sc_dmat, rx_data->map);
> -		m = rx_data->m;
> -		rx_data->m = NULL;
> -		setbit(rx_ring->freemap, idx);
> +		/*
> +		 * Sync the DMA buffer for CPU read before reading the
> +		 * descriptor / MSDU data.
> +		 */
> +		bus_dmamap_sync(sc->sc_dmat, desc_info->map, 0,
> +		    desc_info->map->dm_mapsize, BUS_DMASYNC_POSTREAD);
> +		bus_dmamap_unload(sc->sc_dmat, desc_info->map);
> +		m = desc_info->m;
> +		desc_info->m = NULL;
> +		desc_info->in_use = 0;
>  
> +		/* WCN7850 is single-radio */
> +		mac_id = 0;
>  		num_buffs_reaped[mac_id]++;
>  
> +		TAILQ_INSERT_TAIL(&rx_desc_used_list, desc_info, entry);
> +
>  		push_reason = FIELD_GET(HAL_REO_DEST_RING_INFO0_PUSH_REASON,
>  		    desc->info0);
>  		if (push_reason !=
>  		    HAL_REO_DEST_RING_PUSH_REASON_ROUTING_INSTRUCTION) {
>  			m_freem(m);
> -#if 0
> -			sc->soc_stats.hal_reo_error[
> -			    dp->reo_dst_ring[ring_id].ring_id]++;
> -#endif
>  			continue;
>  		}
>  
> -		msdu = &rx_data->rx_msdu;
> +		msdu = &desc_info->rx_msdu;
>  		msdu->m = m;
>  		msdu->is_first_msdu = !!(desc->rx_msdu_info.info0 &
>  		    RX_MSDU_DESC_INFO0_FIRST_MSDU_IN_MPDU);
> @@ -15010,8 +15226,7 @@ try_again:
>  		    desc->rx_mpdu_info.meta_data);
>  		msdu->seq_no = FIELD_GET(RX_MPDU_DESC_INFO0_SEQ_NUM,
>  		    desc->rx_mpdu_info.info0);
> -		msdu->tid = FIELD_GET(HAL_REO_DEST_RING_INFO0_RX_QUEUE_NUM,
> -		    desc->info0);
> +		msdu->tid = 0; /* no RX_QUEUE_NUM in wifi7 */
>  
>  		msdu->mac_id = mac_id;
>  		TAILQ_INSERT_TAIL(&msdu_list[mac_id], msdu, entry);
> @@ -15030,7 +15245,7 @@ try_again:
>  	 * head pointer so that we can reap complete MPDU in the current
>  	 * rx processing.
>  	 */
> -	if (!done && qwz_hal_srng_dst_num_free(sc, srng, 1)) {
> +	if (!done && budget > 0 && qwz_hal_srng_dst_num_free(sc, srng, 1)) {
>  		qwz_hal_srng_access_end(sc, srng);
>  		goto try_again;
>  	}
> @@ -15048,16 +15263,11 @@ try_again:
>  
>  		qwz_dp_rx_process_received_packets(sc, &msdu_list[i], i);
>  
> -		rx_ring = &sc->pdev_dp.rx_refill_buf_ring;
> -
> -		qwz_dp_rxbufs_replenish(sc, i, rx_ring, num_buffs_reaped[i],
> -		    sc->hw_params.hal_params->rx_buf_rbm);
> +		qwz_dp_rxbufs_replenish(sc, &dp->rx_refill_buf_ring,
> +		    &rx_desc_used_list, num_buffs_reaped[i]);
>  	}
>  exit:
>  	return total_msdu_reaped;
> -#endif
> -	printf("%s:%d\n", __func__, __LINE__);
> -	return 0;
>  }
>  
>  #if 0
> @@ -15361,6 +15571,7 @@ qwz_dp_wmask_compaction_rx_tlv_supported
>  void
>  qwz_dp_hal_rx_desc_init(struct qwz_softc *sc)
>  {
> +	sc->hal_rx_ops = &hal_rx_wcn7850_ops;
>  	if (qwz_dp_wmask_compaction_rx_tlv_supported(sc)) {
>  		/* RX TLVS compaction is supported, hence change the hal_rx_ops
>  		 * to compact hal_rx_ops.
> @@ -21045,6 +21256,7 @@ qwz_vif_alloc(struct qwz_softc *sc)
>  	arvif = malloc(sizeof(*arvif), M_DEVBUF, M_NOWAIT | M_ZERO);
>  	if (arvif == NULL)
>  		return NULL;
> +	arvif->bank_id = -1;
>  
>  	txmgmt = &arvif->txmgmt;
>  	for (i = 0; i < nitems(txmgmt->data); i++) {
> @@ -22061,7 +22273,7 @@ qwz_peer_frags_flush(struct qwz_softc *s
>  #ifdef notyet
>  	lockdep_assert_held(&ar->ab->base_lock);
>  #endif
> -	for (i = 0; i < IEEE80211_NUM_TID; i++) {
> +	for (i = 0; i <= HAL_DESC_REO_NON_QOS_TID; i++) {
>  		rx_tid = &peer->rx_tid[i];
>  
>  		qwz_dp_rx_frags_cleanup(sc, rx_tid, 1);
> @@ -22081,7 +22293,7 @@ qwz_peer_rx_tid_cleanup(struct qwz_softc
>  #ifdef notyet
>  	lockdep_assert_held(&ar->ab->base_lock);
>  #endif
> -	for (i = 0; i < IEEE80211_NUM_TID; i++) {
> +	for (i = 0; i <= HAL_DESC_REO_NON_QOS_TID; i++) {
>  		rx_tid = &peer->rx_tid[i];
>  
>  		qwz_peer_rx_tid_delete(sc, peer, i);
> @@ -22285,7 +22497,7 @@ qwz_dp_peer_setup(struct qwz_softc *sc, 
>  		return ret;
>  	}
>  
> -	for (tid = 0; tid < IEEE80211_NUM_TID; tid++) {
> +	for (tid = 0; tid <= HAL_DESC_REO_NON_QOS_TID; tid++) {
>  		ret = qwz_peer_rx_tid_setup(sc, ni, vdev_id, pdev_id,
>  		    tid, 1, 0, HAL_PN_TYPE_NONE);
>  		if (ret) {
> @@ -22413,6 +22625,14 @@ qwz_dp_tx_get_tid(struct mbuf *m)
>  	return tid;
>  }
>  
> +/*
> + * Build a WCN7850 / ath12k WiFi7 TCL data descriptor. Encap/encrypt/
> + * search settings live in the bank addressed by ti->bank_id; the
> + * descriptor only carries DESC_TYPE | BANK_ID + per-frame fields.
> + * Each TCL_DATA ring slot is just a hal_tcl_data_cmd (no per-entry
> + * TLV header, unlike ath11k); FW reads info0 from offset 0.
> + * Mirror of Linux ath12k_wifi7_hal_tx_cmd_desc_setup().
> + */
>  void
>  qwz_hal_tx_cmd_desc_setup(struct qwz_softc *sc, void *cmd,
>      struct hal_tx_info *ti)
> @@ -22429,29 +22649,24 @@ qwz_hal_tx_cmd_desc_setup(struct qwz_sof
>  
>  	tcl_cmd->info0 =
>  	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_DESC_TYPE, ti->type) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_ENCAP_TYPE, ti->encap_type) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_ENCRYPT_TYPE, ti->encrypt_type) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_SEARCH_TYPE, ti->search_type) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_ADDR_EN, ti->addr_search_flags) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_CMD_NUM, ti->meta_data_flags);
> -
> -	tcl_cmd->info1 = ti->flags0 |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO1_DATA_LEN, ti->data_len) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO1_PKT_OFFSET, ti->pkt_offset);
> -
> -	tcl_cmd->info2 = ti->flags1 |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO2_TID, ti->tid) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO2_LMAC_ID, ti->lmac_id);
> -
> -	tcl_cmd->info3 = FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_DSCP_TID_TABLE_IDX,
> -	    ti->dscp_tid_tbl_idx) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_SEARCH_INDEX, ti->bss_ast_idx) |
> -	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_CACHE_SET_NUM, ti->bss_ast_hash);
> -	tcl_cmd->info4 = 0;
> -#ifdef notyet
> -	if (ti->enable_mesh)
> -		ab->hw_params.hw_ops->tx_mesh_enable(ab, tcl_cmd);
> -#endif
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO0_BANK_ID, ti->bank_id);
> +
> +	tcl_cmd->info1 = FIELD_PREP(HAL_TCL_DATA_CMD_INFO1_CMD_NUM,
> +	    ti->meta_data_flags);
> +
> +	tcl_cmd->info2 = ti->flags0 |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO2_DATA_LEN, ti->data_len) |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO2_PKT_OFFSET, ti->pkt_offset);
> +
> +	tcl_cmd->info3 = ti->flags1 |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_TID, ti->tid) |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_PMAC_ID, ti->lmac_id) |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_VDEV_ID, ti->vdev_id);
> +
> +	tcl_cmd->info4 =
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO4_SEARCH_INDEX, ti->bss_ast_idx) |
> +	    FIELD_PREP(HAL_TCL_DATA_CMD_INFO4_CACHE_SET_NUM, ti->bss_ast_hash);
> +	tcl_cmd->info5 = 0;
>  }
>  
>  int
> @@ -22477,6 +22692,23 @@ qwz_dp_tx(struct qwz_softc *sc, struct q
>  		m_freem(m);
>  		return ESHUTDOWN;
>  	}
> +
> +	if (arvif->bank_id < 0) {
> +		printf("%s: TX before TX bank is configured, dropping\n",
> +		    sc->sc_dev.dv_xname);
> +		m_freem(m);
> +		return EIO;
> +	}
> +
> +	/*
> +	 * Default to no encryption. enum hal_encrypt_type starts with
> +	 * HAL_ENCRYPT_TYPE_WEP_40 = 0, and the {0} initializer would
> +	 * otherwise silently select WEP40.
> +	 */
> +	ti.encrypt_type = HAL_ENCRYPT_TYPE_OPEN;
> +	ti.bank_id = arvif->bank_id;
> +	ti.vdev_id = arvif->vdev_id;
> +
>  #if 0
>  	if (unlikely(!(info->flags & IEEE80211_TX_CTL_HW_80211_ENCAP) &&
>  		     !ieee80211_is_data(hdr->frame_control)))
> @@ -22570,7 +22802,7 @@ qwz_dp_tx(struct qwz_softc *sc, struct q
>  	if (ieee80211_vif_is_mesh(arvif->vif))
>  		ti.enable_mesh = true;
>  #endif
> -	ti.flags1 |= FIELD_PREP(HAL_TCL_DATA_CMD_INFO2_TID_OVERWRITE, 1);
> +	ti.flags1 |= FIELD_PREP(HAL_TCL_DATA_CMD_INFO3_TID_OVERWRITE, 1);
>  
>  	ti.tid = qwz_dp_tx_get_tid(m);
>  #if 0
> @@ -22620,6 +22852,9 @@ qwz_dp_tx(struct qwz_softc *sc, struct q
>  	}
>  	ti.paddr = tx_data->map->dm_segs[0].ds_addr;
>  
> +	bus_dmamap_sync(sc->sc_dmat, tx_data->map, 0,
> +	    tx_data->map->dm_mapsize, BUS_DMASYNC_PREWRITE);
> +
>  	ti.data_len = m->m_pkthdr.len;
>  
>  	hal_ring_id = tx_ring->tcl_data_ring.ring_id;
> @@ -22649,8 +22884,7 @@ qwz_dp_tx(struct qwz_softc *sc, struct q
>  	tx_data->m = m;
>  	tx_data->ni = ni;
>  
> -	qwz_hal_tx_cmd_desc_setup(sc,
> -	    hal_tcl_desc + sizeof(struct hal_tlv_hdr), &ti);
> +	qwz_hal_tx_cmd_desc_setup(sc, hal_tcl_desc, &ti);
>  
>  	qwz_hal_srng_access_end(sc, tcl_ring);
>  
> @@ -23404,7 +23638,7 @@ qwz_peer_assoc_h_basic(struct qwz_softc 
>  
>  	IEEE80211_ADDR_COPY(arg->peer_mac, ni->ni_macaddr);
>  	arg->vdev_id = arvif->vdev_id;
> -	arg->peer_associd = ni->ni_associd;
> +	arg->peer_associd = IEEE80211_AID(ni->ni_associd);
>  	arg->auth_flag = 1;
>  	arg->peer_listen_intval = ni->ni_intval;
>  	arg->peer_nss = 1;
> @@ -23564,7 +23798,12 @@ qwz_run(struct qwz_softc *sc)
>  	WARN_ON(arvif->is_up);
>  #endif
>  
> -	arvif->aid = ni->ni_associd;
> +	/*
> +	 * ni->ni_associd carries the raw on-wire AID field including the
> +	 * top two reserved bits (which are always 11 on the wire). The FW
> +	 * wants the 14-bit AID value only.
> +	 */
> +	arvif->aid = IEEE80211_AID(ni->ni_associd);
>  	IEEE80211_ADDR_COPY(arvif->bssid, ni->ni_bssid);
>  
>  	ret = qwz_wmi_vdev_up(sc, arvif->vdev_id, pdev_id, arvif->aid,
> @@ -23582,14 +23821,6 @@ qwz_run(struct qwz_softc *sc)
>  
>  	DNPRINTF(QWZ_D_MAC, "%s: vdev %d up (associated) bssid %s aid %d\n",
>  	    __func__, arvif->vdev_id, ether_sprintf(ni->ni_bssid), arvif->aid);
> -
> -	ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr, arvif->vdev_id,
> -	    pdev_id, WMI_PEER_AUTHORIZE, 1);
> -	if (ret) {
> -		printf("%s: unable to authorize BSS peer: %d\n",
> -		   sc->sc_dev.dv_xname, ret);
> -		return ret;
> -	}
>  
>  	/* Enable "ext" IRQs for datapath. */
>  	sc->ops.irq_enable(sc);
> Index: sys/dev/ic/qwzreg.h
> ===================================================================
> RCS file: /cvs/src/sys/dev/ic/qwzreg.h,v
> diff -u -p -u -p -r1.12 qwzreg.h
> --- sys/dev/ic/qwzreg.h	12 Apr 2026 19:52:23 -0000	1.12
> +++ sys/dev/ic/qwzreg.h	25 Apr 2026 21:29:58 -0000
> @@ -8236,7 +8236,6 @@ enum hal_rx_msdu_desc_reo_dest_ind {
>  
>  struct rx_msdu_desc {
>  	uint32_t info0;
> -	uint32_t rsvd0;
>  } __packed;
>  
>  /* rx_msdu_desc
> @@ -8406,33 +8405,21 @@ enum hal_reo_dest_ring_error_code {
>  	HAL_REO_DEST_RING_ERROR_CODE_MAX,
>  };
>  
> -#define HAL_REO_DEST_RING_INFO0_QUEUE_ADDR_HI		GENMASK(7, 0)
> -#define HAL_REO_DEST_RING_INFO0_BUFFER_TYPE		(1 << 8)
> -#define HAL_REO_DEST_RING_INFO0_PUSH_REASON		GENMASK(10, 9)
> -#define HAL_REO_DEST_RING_INFO0_ERROR_CODE		GENMASK(15, 11)
> -#define HAL_REO_DEST_RING_INFO0_RX_QUEUE_NUM		GENMASK(31, 16)
> -
> -#define HAL_REO_DEST_RING_INFO1_REORDER_INFO_VALID	(1 << 0)
> -#define HAL_REO_DEST_RING_INFO1_REORDER_OPCODE		GENMASK(4, 1)
> -#define HAL_REO_DEST_RING_INFO1_REORDER_SLOT_IDX	GENMASK(12, 5)
> -
> -#define HAL_REO_DEST_RING_INFO2_RING_ID			GENMASK(27, 20)
> -#define HAL_REO_DEST_RING_INFO2_LOOPING_COUNT		GENMASK(31, 28)
> +/* wifi7 / WCN7850 REO destination ring info0 */
> +#define HAL_REO_DEST_RING_INFO0_PUSH_REASON		GENMASK(2, 1)
> +#define HAL_REO_DEST_RING_INFO0_ERROR_CODE		GENMASK(7, 3)
> +#define HAL_REO_DEST_RING_INFO0_BUFFER_TYPE		BIT(0)
> +#define HAL_REO_DEST_RING_INFO0_SRC_LINK_ID		GENMASK(15, 13)
> +#define HAL_REO_DEST_RING_INFO0_RING_ID			GENMASK(27, 20)
> +#define HAL_REO_DEST_RING_INFO0_LOOPING_COUNT		GENMASK(31, 28)
>  
>  struct hal_reo_dest_ring {
>  	struct ath12k_buffer_addr buf_addr_info;
>  	struct rx_mpdu_desc rx_mpdu_info;
>  	struct rx_msdu_desc rx_msdu_info;
> -	uint32_t queue_addr_lo;
> +	uint32_t buf_va_lo;
> +	uint32_t buf_va_hi;
>  	uint32_t info0; /* %HAL_REO_DEST_RING_INFO0_ */
> -	uint32_t info1; /* %HAL_REO_DEST_RING_INFO1_ */
> -	uint32_t rsvd0;
> -	uint32_t rsvd1;
> -	uint32_t rsvd2;
> -	uint32_t rsvd3;
> -	uint32_t rsvd4;
> -	uint32_t rsvd5;
> -	uint32_t info2; /* %HAL_REO_DEST_RING_INFO2_ */
>  } __packed;
>  
>  /* hal_reo_dest_ring
> @@ -8843,39 +8830,61 @@ struct hal_reo_flush_cache {
>  	uint32_t rsvd0[6];
>  } __packed;
>  
> -#define HAL_TCL_DATA_CMD_INFO0_DESC_TYPE	BIT(0)
> -#define HAL_TCL_DATA_CMD_INFO0_EPD		BIT(1)
> -#define HAL_TCL_DATA_CMD_INFO0_ENCAP_TYPE	GENMASK(3, 2)
> -#define HAL_TCL_DATA_CMD_INFO0_ENCRYPT_TYPE	GENMASK(7, 4)
> -#define HAL_TCL_DATA_CMD_INFO0_SRC_BUF_SWAP	BIT(8)
> -#define HAL_TCL_DATA_CMD_INFO0_LNK_META_SWAP	BIT(9)
> -#define HAL_TCL_DATA_CMD_INFO0_SEARCH_TYPE	GENMASK(13, 12)
> -#define HAL_TCL_DATA_CMD_INFO0_ADDR_EN		GENMASK(15, 14)
> -#define HAL_TCL_DATA_CMD_INFO0_CMD_NUM		GENMASK(31, 16)
> -
> -#define HAL_TCL_DATA_CMD_INFO1_DATA_LEN		GENMASK(15, 0)
> -#define HAL_TCL_DATA_CMD_INFO1_IP4_CKSUM_EN	BIT(16)
> -#define HAL_TCL_DATA_CMD_INFO1_UDP4_CKSUM_EN	BIT(17)
> -#define HAL_TCL_DATA_CMD_INFO1_UDP6_CKSUM_EN	BIT(18)
> -#define HAL_TCL_DATA_CMD_INFO1_TCP4_CKSUM_EN	BIT(19)
> -#define HAL_TCL_DATA_CMD_INFO1_TCP6_CKSUM_EN	BIT(20)
> -#define HAL_TCL_DATA_CMD_INFO1_TO_FW		BIT(21)
> -#define HAL_TCL_DATA_CMD_INFO1_PKT_OFFSET	GENMASK(31, 23)
> -
> -#define HAL_TCL_DATA_CMD_INFO2_BUF_TIMESTAMP		GENMASK(18, 0)
> -#define HAL_TCL_DATA_CMD_INFO2_BUF_T_VALID		BIT(19)
> -#define HAL_IPQ8074_TCL_DATA_CMD_INFO2_MESH_ENABLE	BIT(20)
> -#define HAL_TCL_DATA_CMD_INFO2_TID_OVERWRITE		BIT(21)
> -#define HAL_TCL_DATA_CMD_INFO2_TID			GENMASK(25, 22)
> -#define HAL_TCL_DATA_CMD_INFO2_LMAC_ID			GENMASK(27, 26)
> -
> -#define HAL_TCL_DATA_CMD_INFO3_DSCP_TID_TABLE_IDX	GENMASK(5, 0)
> -#define HAL_TCL_DATA_CMD_INFO3_SEARCH_INDEX		GENMASK(25, 6)
> -#define HAL_TCL_DATA_CMD_INFO3_CACHE_SET_NUM		GENMASK(29, 26)
> -#define HAL_QCN9074_TCL_DATA_CMD_INFO3_MESH_ENABLE	GENMASK(31, 30)
> -
> -#define HAL_TCL_DATA_CMD_INFO4_RING_ID			GENMASK(27, 20)
> -#define HAL_TCL_DATA_CMD_INFO4_LOOPING_COUNT		GENMASK(31, 28)
> +/*
> + * WCN7850 / ath12k WiFi7 TCL data descriptor layout. The OLDER
> + * ath11k/ath12k-non-WiFi7 layout had encap_type/encrypt_type/search_type/
> + * addr_en encoded inline in info0; the WiFi7 hardware moves these to a
> + * pre-configured TX bank addressed by bank_id. The descriptor itself
> + * carries DESC_TYPE | BANK_ID | DATA_LEN | TID | VDEV_ID | search_index.
> + */
> +#define HAL_TCL_DATA_CMD_INFO0_CMD_TYPE		BIT(0)
> +#define HAL_TCL_DATA_CMD_INFO0_DESC_TYPE	BIT(1)
> +#define HAL_TCL_DATA_CMD_INFO0_BANK_ID		GENMASK(7, 2)
> +#define HAL_TCL_DATA_CMD_INFO0_TX_NOTIFY_FRAME	GENMASK(10, 8)
> +#define HAL_TCL_DATA_CMD_INFO0_HDR_LEN_READ_SEL	BIT(11)
> +#define HAL_TCL_DATA_CMD_INFO0_BUF_TIMESTAMP	GENMASK(30, 12)
> +#define HAL_TCL_DATA_CMD_INFO0_BUF_TIMESTAMP_VLD	BIT(31)
> +
> +#define HAL_TCL_DATA_CMD_INFO1_CMD_NUM		GENMASK(31, 16)
> +
> +#define HAL_TCL_DATA_CMD_INFO2_DATA_LEN		GENMASK(15, 0)
> +#define HAL_TCL_DATA_CMD_INFO2_IP4_CKSUM_EN	BIT(16)
> +#define HAL_TCL_DATA_CMD_INFO2_UDP4_CKSUM_EN	BIT(17)
> +#define HAL_TCL_DATA_CMD_INFO2_UDP6_CKSUM_EN	BIT(18)
> +#define HAL_TCL_DATA_CMD_INFO2_TCP4_CKSUM_EN	BIT(19)
> +#define HAL_TCL_DATA_CMD_INFO2_TCP6_CKSUM_EN	BIT(20)
> +#define HAL_TCL_DATA_CMD_INFO2_TO_FW		BIT(21)
> +#define HAL_TCL_DATA_CMD_INFO2_PKT_OFFSET	GENMASK(31, 23)
> +
> +#define HAL_TCL_DATA_CMD_INFO3_TID_OVERWRITE	BIT(0)
> +#define HAL_TCL_DATA_CMD_INFO3_FLOW_OVERRIDE_EN	BIT(1)
> +#define HAL_TCL_DATA_CMD_INFO3_CLASSIFY_INFO_SEL	GENMASK(3, 2)
> +#define HAL_TCL_DATA_CMD_INFO3_TID		GENMASK(7, 4)
> +#define HAL_TCL_DATA_CMD_INFO3_FLOW_OVERRIDE	BIT(8)
> +#define HAL_TCL_DATA_CMD_INFO3_PMAC_ID		GENMASK(10, 9)
> +#define HAL_TCL_DATA_CMD_INFO3_MSDU_COLOR	GENMASK(12, 11)
> +#define HAL_TCL_DATA_CMD_INFO3_VDEV_ID		GENMASK(31, 24)
> +
> +#define HAL_TCL_DATA_CMD_INFO4_SEARCH_INDEX	GENMASK(19, 0)
> +#define HAL_TCL_DATA_CMD_INFO4_CACHE_SET_NUM	GENMASK(23, 20)
> +#define HAL_TCL_DATA_CMD_INFO4_IDX_LOOKUP_OVERRIDE	BIT(24)
> +
> +#define HAL_TCL_DATA_CMD_INFO5_RING_ID		GENMASK(27, 20)
> +#define HAL_TCL_DATA_CMD_INFO5_LOOPING_COUNT	GENMASK(31, 28)
> +
> +/* Per-bank config for HAL_TCL_SW_CONFIG_BANK_ADDR + 4 * bank_id. */
> +#define HAL_TX_BANK_CONFIG_EPD			BIT(0)
> +#define HAL_TX_BANK_CONFIG_ENCAP_TYPE		GENMASK(2, 1)
> +#define HAL_TX_BANK_CONFIG_ENCRYPT_TYPE		GENMASK(6, 3)
> +#define HAL_TX_BANK_CONFIG_SRC_BUFFER_SWAP	BIT(7)
> +#define HAL_TX_BANK_CONFIG_LINK_META_SWAP	BIT(8)
> +#define HAL_TX_BANK_CONFIG_INDEX_LOOKUP_EN	BIT(9)
> +#define HAL_TX_BANK_CONFIG_ADDRX_EN		BIT(10)
> +#define HAL_TX_BANK_CONFIG_ADDRY_EN		BIT(11)
> +#define HAL_TX_BANK_CONFIG_MESH_EN		GENMASK(13, 12)
> +#define HAL_TX_BANK_CONFIG_VDEV_ID_CHECK_EN	BIT(14)
> +#define HAL_TX_BANK_CONFIG_PMAC_ID		GENMASK(16, 15)
> +#define HAL_TX_BANK_CONFIG_DSCP_TIP_MAP_ID	GENMASK(22, 17)
>  
>  enum hal_encrypt_type {
>  	HAL_ENCRYPT_TYPE_WEP_40,
> @@ -12381,7 +12390,13 @@ struct hal_rx_desc_qcn9274_compact {
>  	uint8_t msdu_payload[];
>  } __packed;
>  
> -#define RX_BE_PADDING0_BYTES 8
> +/*
> + * Empirically observed on WCN7850 hw2.0 fw 0x110cffff: the FW writes
> + * mpdu_start_tag at descriptor offset 216 (not 144 as a literal reading
> + * of upstream Linux/ath12k headers would suggest). The padding between
> + * msdu_end and mpdu_start_tag is 80 bytes, not 8.
> + */
> +#define RX_BE_PADDING0_BYTES 80
>  #define RX_BE_PADDING1_BYTES 8
>  
>  #define HAL_RX_BE_PKT_HDR_TLV_LEN 112
> @@ -13167,6 +13182,7 @@ enum htt_t2h_msg_type {
>  	HTT_T2H_MSG_TYPE_PPDU_STATS_IND = 0x1d,
>  	HTT_T2H_MSG_TYPE_EXT_STATS_CONF = 0x1c,
>  	HTT_T2H_MSG_TYPE_BKPRESSURE_EVENT_IND = 0x24,
> +	HTT_T2H_MSG_TYPE_PRIMARY_LINK_PEER_MIGRATE_IND = 0x30,
>  };
>  
>  #define HTT_TARGET_VERSION_MAJOR 3
> Index: sys/dev/ic/qwzvar.h
> ===================================================================
> RCS file: /cvs/src/sys/dev/ic/qwzvar.h,v
> diff -u -p -u -p -r1.13 qwzvar.h
> --- sys/dev/ic/qwzvar.h	12 Apr 2026 19:52:23 -0000	1.13
> +++ sys/dev/ic/qwzvar.h	25 Apr 2026 21:29:58 -0000
> @@ -141,6 +141,8 @@ struct hal_tx_info {
>  	uint8_t dscp_tid_tbl_idx;
>  	bool enable_mesh;
>  	uint8_t rbm_id;
> +	uint8_t bank_id;	/* WCN7850/WiFi7: pre-configured TX bank index */
> +	uint8_t vdev_id;	/* WCN7850/WiFi7: VDEV ID in tcl_data_cmd info3 */
>  };
>  
>  /* TODO: Check if the actual desc macros can be used instead */
> @@ -281,9 +283,9 @@ struct hal_rx_ops {
>  #ifdef notyet
>  	uint8_t (*rx_desc_get_mesh_ctl)(struct hal_rx_desc *desc);
>  	bool (*rx_desc_get_mpdu_seq_ctl_vld)(struct hal_rx_desc *desc);
> -	bool (*rx_desc_get_mpdu_fc_valid)(struct hal_rx_desc *desc);
>  	uint16_t (*rx_desc_get_mpdu_start_seq_no)(struct hal_rx_desc *desc);
>  #endif
> +	bool (*rx_desc_get_mpdu_fc_valid)(struct hal_rx_desc *desc);
>  	uint16_t (*rx_desc_get_msdu_len)(struct hal_rx_desc *desc);
>  #ifdef notyet
>  	uint8_t (*rx_desc_get_msdu_sgi)(struct hal_rx_desc *desc);
> @@ -1103,6 +1105,7 @@ struct ath12k_rx_desc_info {
>  	uint32_t magic;
>  	uint8_t in_use		: 1,
>  	        reserved	: 7;
> +	struct qwz_rx_msdu rx_msdu;
>  };
>  
>  struct ath12k_tx_desc_info {
> @@ -1798,6 +1801,7 @@ struct qwz_vif {
>  	uint16_t tcl_metadata;
>  	uint8_t hal_addr_search_flags;
>  	uint8_t search_type;
> +	int8_t bank_id;	/* WCN7850/WiFi7 TX bank profile id, -1 if unset */
>  
>  	struct qwz_softc *sc;
>  
> Index: sys/dev/pci/if_qwz_pci.c
> ===================================================================
> RCS file: /cvs/src/sys/dev/pci/if_qwz_pci.c,v
> diff -u -p -u -p -r1.7 if_qwz_pci.c
> --- sys/dev/pci/if_qwz_pci.c	12 Apr 2026 19:52:23 -0000	1.7
> +++ sys/dev/pci/if_qwz_pci.c	25 Apr 2026 21:29:58 -0000
> @@ -1490,9 +1490,10 @@ qwz_pcic_ext_irq_config(struct qwz_softc
>  
>  		if (num_irq) {
>  			int irq_idx = irq_grp->irqs[0];
> +			int vector = (i % num_vectors) + base_vector;
>  			pci_intr_handle_t ih;
>  
> -			if (pci_intr_map_msivec(pa, irq_idx, &ih) != 0 &&
> +			if (pci_intr_map_msivec(pa, vector, &ih) != 0 &&
>  			    pci_intr_map(pa, &ih) != 0) {
>  				printf("%s: can't map interrupt\n",
>  				    sc->sc_dev.dv_xname);
>