[PATCH v2] wifi: ath9k_htc: pass CRC-tagged spectral samples to the FFT parser
From: Nerijus Bendžiūnas
Date: Fri Sep 04 2026 - 14:36:52 EST
The AR9271 firmware checks AR_CRCErr before AR_PHYErr when it fills in
the RX status, so a descriptor with both bits set reaches the host as a
CRC error without the PHY error flag. ath9k had the same order and
changed it in commit 3a325565c7fa ("ath9k: reorder error codes for
spectral"), because spectral samples received under interference often
carry a CRC error. The firmware was never updated, and the host passes
only PHY errors to ath_cmn_process_fft(), so on a busy channel the scan
keeps producing samples and the host drops all of them as CRC errors.
Observed on a deployed receiver: 640 frames per second with rs_status
0x01, each with SPECTRAL_SCAN_BITMASK set in the trailing radar_info,
while the recv CRC ERR counter grew at the sample rate and PHY ERR did
not move.
When a scan is active, also pass a CRC error frame to the parser when
its length is one an FFT report can have, one byte less to two bytes
more than the report length for the channel width, with the PHY error
code the parser expects. The parser still checks SPECTRAL_SCAN_BITMASK
in the trailer and returns a frame without it to the normal path. A
frame with the bit set is consumed whether or not its contents parse.
It had failed its CRC and was about to be dropped anyway; the only
visible change is that a monitor interface with FIF_FCSFAIL no longer
sees those frames.
The firmware can be fixed separately, but linux-firmware ships version
1.4.0 from 2015, so the host has to handle what that firmware sends.
Fixes: 83fb287ecd8a ("ath9k_htc: process rx spectral packets")
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Nerijus Bendžiūnas <nerijus.bendziunas@xxxxxxxxx>
---
Changes in v2:
- Describe what the parser actually checks: after the forced error code
its only remaining gate is SPECTRAL_SCAN_BITMASK; a frame passing it
is consumed.
- Comment and length test now say the accepted window is one byte less
to two bytes more than the report length, which is what the parser
accepts; the test was already that, written as len + 1 >= fft_len.
- Pass the parser a copy of the rx status instead of changing the
original in place.
- Add Assisted-by, rewrite the commit message, rebase onto ath-next.
drivers/net/wireless/ath/ath9k/htc_drv_txrx.c | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/drivers/net/wireless/ath/ath9k/htc_drv_txrx.c b/drivers/net/wireless/ath/ath9k/htc_drv_txrx.c
index bed7ea2425a0..b0b95444fc95 100644
--- a/drivers/net/wireless/ath/ath9k/htc_drv_txrx.c
+++ b/drivers/net/wireless/ath/ath9k/htc_drv_txrx.c
@@ -969,6 +969,32 @@ static void rx_status_htc_to_ath(struct ath_rx_status *rx_stats,
convert_htc_flag(rx_stats, rxstatus);
}
+/*
+ * The firmware reports a frame that failed its CRC as a CRC error even when
+ * the PHY error bit is set as well, so under interference spectral samples
+ * reach the host as CRC errors. A sample is recognisable by its size: one
+ * byte less to two bytes more than the FFT report length for the channel
+ * width, the range the parser accepts.
+ */
+static bool ath9k_htc_is_spectral_sample_len(struct ath9k_htc_priv *priv,
+ u16 len)
+{
+ enum nl80211_channel_type chan_type;
+ u16 fft_len;
+
+ if (priv->spec_priv.spectral_mode == SPECTRAL_DISABLED)
+ return false;
+
+ chan_type = cfg80211_get_chandef_type(&priv->hw->conf.chandef);
+ if (chan_type == NL80211_CHAN_HT40MINUS ||
+ chan_type == NL80211_CHAN_HT40PLUS)
+ fft_len = SPECTRAL_HT20_40_TOTAL_DATA_LEN;
+ else
+ fft_len = SPECTRAL_HT20_TOTAL_DATA_LEN;
+
+ return len >= fft_len - 1 && len <= fft_len + 2;
+}
+
static bool ath9k_rx_prepare(struct ath9k_htc_priv *priv,
struct ath9k_htc_rxbuf *rxbuf,
struct ieee80211_rx_status *rx_status)
@@ -1052,6 +1078,21 @@ static bool ath9k_rx_prepare(struct ath9k_htc_priv *priv,
goto rx_next;
}
+ /*
+ * Hand a CRC error of sample size to the FFT parser with the error
+ * code it expects. It returns 0 only for a frame without the spectral
+ * bit in its trailer; anything else is consumed.
+ */
+ if (unlikely(rx_stats.rs_status & ATH9K_RXERR_CRC) &&
+ ath9k_htc_is_spectral_sample_len(priv, rs_datalen)) {
+ struct ath_rx_status sample_rs = rx_stats;
+
+ sample_rs.rs_phyerr = ATH9K_PHYERR_RADAR;
+ if (ath_cmn_process_fft(&priv->spec_priv, hdr, &sample_rs,
+ rx_status->mactime))
+ goto rx_next;
+ }
+
if (!ath9k_cmn_rx_accept(common, hdr, rx_status, &rx_stats,
&decrypt_error, priv->rxfilter))
goto rx_next;
base-commit: 1d8e73163ef933624341075f576e2f36ef9133f7
--
2.55.0