mutex_unlock(&mvm->mutex);
 }
 
+static int iwl_mvm_mac_get_acs_survey(struct iwl_mvm *mvm, int idx,
+                                     struct survey_info *survey)
+{
+       int chan_idx;
+       enum nl80211_band band;
+       int ret;
+
+       mutex_lock(&mvm->mutex);
+
+       if (!mvm->acs_survey) {
+               ret = -ENOENT;
+               goto out;
+       }
+
+       /* Find and return the next entry that has a non-zero active time */
+       for (band = 0; band < NUM_NL80211_BANDS; band++) {
+               struct ieee80211_supported_band *sband =
+                       mvm->hw->wiphy->bands[band];
+
+               if (!sband)
+                       continue;
+
+               for (chan_idx = 0; chan_idx < sband->n_channels; chan_idx++) {
+                       struct iwl_mvm_acs_survey_channel *info =
+                               &mvm->acs_survey->bands[band][chan_idx];
+
+                       if (!info->time)
+                               continue;
+
+                       /* Found (the next) channel to report */
+                       survey->channel = &sband->channels[chan_idx];
+                       survey->filled = SURVEY_INFO_TIME |
+                                        SURVEY_INFO_TIME_BUSY |
+                                        SURVEY_INFO_TIME_RX |
+                                        SURVEY_INFO_TIME_TX;
+                       survey->time = info->time;
+                       survey->time_busy = info->time_busy;
+                       survey->time_rx = info->time_rx;
+                       survey->time_tx = info->time_tx;
+                       survey->noise = info->noise;
+                       if (survey->noise < 0)
+                               survey->filled |= SURVEY_INFO_NOISE_DBM;
+
+                       /* Clear time so that channel is only reported once */
+                       info->time = 0;
+
+                       ret = 0;
+                       goto out;
+               }
+       }
+
+       ret = -ENOENT;
+
+out:
+       mutex_unlock(&mvm->mutex);
+
+       return ret;
+}
+
 int iwl_mvm_mac_get_survey(struct ieee80211_hw *hw, int idx,
                           struct survey_info *survey)
 {
 
        memset(survey, 0, sizeof(*survey));
 
-       /* only support global statistics right now */
-       if (idx != 0)
-               return -ENOENT;
-
        if (!fw_has_capa(&mvm->fw->ucode_capa,
                         IWL_UCODE_TLV_CAPA_RADIO_BEACON_STATS))
                return -ENOENT;
 
+       /*
+        * Return the beacon stats at index zero and pass on following indices
+        * to the function returning the full survey, most likely for ACS
+        * (Automatic Channel Selection).
+        */
+       if (idx > 0)
+               return iwl_mvm_mac_get_acs_survey(mvm, idx - 1, survey);
+
        mutex_lock(&mvm->mutex);
 
        if (iwl_mvm_firmware_running(mvm)) {
 
        struct work_struct scan_work;
 };
 
+/**
+ * struct iwl_mvm_acs_survey_channel - per-channel survey information
+ *
+ * Stripped down version of &struct survey_info.
+ *
+ * @time: time in ms the radio was on the channel
+ * @time_busy: time in ms the channel was sensed busy
+ * @time_tx: time in ms spent transmitting data
+ * @time_rx: time in ms spent receiving data
+ * @noise: channel noise in dBm
+ */
+struct iwl_mvm_acs_survey_channel {
+       u32 time;
+       u32 time_busy;
+       u32 time_tx;
+       u32 time_rx;
+       s8 noise;
+};
+
+struct iwl_mvm_acs_survey {
+       struct iwl_mvm_acs_survey_channel *bands[NUM_NL80211_BANDS];
+
+       /* Overall number of channels */
+       int n_channels;
+
+       /* Storage space for per-channel information follows */
+       struct iwl_mvm_acs_survey_channel channels[] __counted_by(n_channels);
+};
+
 struct iwl_mvm {
        /* for logger access */
        struct device *dev;
 
        struct iwl_mei_scan_filter mei_scan_filter;
 
+       struct iwl_mvm_acs_survey *acs_survey;
+
        bool statistics_clear;
 };
 
 bool iwl_mvm_mld_valid_link_pair(struct ieee80211_vif *vif,
                                 const struct iwl_mvm_link_sel_data *a,
                                 const struct iwl_mvm_link_sel_data *b);
+
+s8 iwl_mvm_average_dbm_values(const struct iwl_umac_scan_channel_survey_notif *notif);
 #endif
 
 /* AP and IBSS */
 void iwl_mvm_report_scan_aborted(struct iwl_mvm *mvm);
 void iwl_mvm_scan_timeout_wk(struct work_struct *work);
 int iwl_mvm_int_mlo_scan(struct iwl_mvm *mvm, struct ieee80211_vif *vif);
+void iwl_mvm_rx_channel_survey_notif(struct iwl_mvm *mvm,
+                                    struct iwl_rx_cmd_buffer *rxb);
 
 /* Scheduled scan */
 void iwl_mvm_rx_lmac_scan_complete_notif(struct iwl_mvm *mvm,
 
        RX_HANDLER_GRP(MAC_CONF_GROUP, ROC_NOTIF,
                       iwl_mvm_rx_roc_notif, RX_HANDLER_SYNC,
                       struct iwl_roc_notif),
+       RX_HANDLER_GRP(SCAN_GROUP, CHANNEL_SURVEY_NOTIF,
+                      iwl_mvm_rx_channel_survey_notif, RX_HANDLER_ASYNC_LOCKED,
+                      struct iwl_umac_scan_channel_survey_notif),
 };
 #undef RX_HANDLER
 #undef RX_HANDLER_GRP
        kfree(mvm->temp_nvm_data);
        for (i = 0; i < NVM_MAX_NUM_SECTIONS; i++)
                kfree(mvm->nvm_sections[i].data);
+       kfree(mvm->acs_survey);
 
        cancel_delayed_work_sync(&mvm->tcm.work);
 
 
                .global_cnt = 0,
        };
 
+       /*
+        * A scanning AP interface probably wants to generate a survey to do
+        * ACS (automatic channel selection).
+        * Force a non-fragmented scan in that case.
+        */
+       if (vif && ieee80211_vif_type_p2p(vif) == NL80211_IFTYPE_AP)
+               return IWL_SCAN_TYPE_WILD;
+
        ieee80211_iterate_active_interfaces_atomic(mvm->hw,
                                                   IEEE80211_IFACE_ITER_NORMAL,
                                                   iwl_mvm_scan_iterator,
         *      4. it's not a p2p find operation.
         *      5. we are not in low latency mode,
         *         or if fragmented ebs is supported by the FW
+        *      6. the VIF is not an AP interface (scan wants survey results)
         */
        return ((capa->flags & IWL_UCODE_TLV_FLAGS_EBS_SUPPORT) &&
                mvm->last_ebs_successful && IWL_MVM_ENABLE_EBS &&
                vif->type != NL80211_IFTYPE_P2P_DEVICE &&
-               (!low_latency || iwl_mvm_is_frag_ebs_supported(mvm)));
+               (!low_latency || iwl_mvm_is_frag_ebs_supported(mvm)) &&
+               ieee80211_vif_type_p2p(vif) != NL80211_IFTYPE_AP);
 }
 
 static inline bool iwl_mvm_is_regular_scan(struct iwl_mvm_scan_params *params)
 
 static u8 iwl_mvm_scan_umac_flags2(struct iwl_mvm *mvm,
                                   struct iwl_mvm_scan_params *params,
-                                  struct ieee80211_vif *vif, int type)
+                                  struct ieee80211_vif *vif, int type,
+                                  u16 gen_flags)
 {
        u8 flags = 0;
 
                        IWL_UCODE_TLV_CAPA_SCAN_DONT_TOGGLE_ANT))
                flags |= IWL_UMAC_SCAN_GEN_PARAMS_FLAGS2_DONT_TOGGLE_ANT;
 
+       /* Passive and AP interface -> ACS (automatic channel selection) */
+       if (gen_flags & IWL_UMAC_SCAN_GEN_FLAGS_V2_FORCE_PASSIVE &&
+           ieee80211_vif_type_p2p(vif) == NL80211_IFTYPE_AP &&
+           iwl_fw_lookup_notif_ver(mvm->fw, SCAN_GROUP, CHANNEL_SURVEY_NOTIF,
+                                   0) >= 1)
+               flags |= IWL_UMAC_SCAN_GEN_FLAGS2_COLLECT_CHANNEL_STATS;
+
        return flags;
 }
 
        gen_flags = iwl_mvm_scan_umac_flags_v2(mvm, params, vif, type);
 
        if (version >= 15)
-               gen_flags2 = iwl_mvm_scan_umac_flags2(mvm, params, vif, type);
+               gen_flags2 = iwl_mvm_scan_umac_flags2(mvm, params, vif, type,
+                                                     gen_flags);
        else
                gen_flags2 = 0;
 
 
        return iwl_mvm_int_mlo_scan_start(mvm, vif, channels, n_channels);
 }
+
+static int iwl_mvm_chanidx_from_phy(struct iwl_mvm *mvm,
+                                   enum nl80211_band band,
+                                   u16 phy_chan_num)
+{
+       struct ieee80211_supported_band *sband = mvm->hw->wiphy->bands[band];
+       int chan_idx;
+
+       if (WARN_ON_ONCE(!sband))
+               return -EINVAL;
+
+       for (chan_idx = 0; chan_idx < sband->n_channels; chan_idx++) {
+               struct ieee80211_channel *channel = &sband->channels[chan_idx];
+
+               if (channel->hw_value == phy_chan_num)
+                       return chan_idx;
+       }
+
+       return -EINVAL;
+}
+
+static u32 iwl_mvm_div_by_db(u32 value, u8 db)
+{
+       /*
+        * 2^32 * 10**(i / 10) for i = [1, 10], skipping 0 and simply stopping
+        * at 10 dB and looping instead of using a much larger table.
+        *
+        * Using 64 bit math is overkill, but means the helper does not require
+        * a limit on the input range.
+        */
+       static const u32 db_to_val[] = {
+               0xcb59185e, 0xa1866ba8, 0x804dce7a, 0x65ea59fe, 0x50f44d89,
+               0x404de61f, 0x331426af, 0x2892c18b, 0x203a7e5b, 0x1999999a,
+       };
+
+       while (value && db > 0) {
+               u8 change = min_t(u8, db, ARRAY_SIZE(db_to_val));
+
+               value = (((u64)value) * db_to_val[change - 1]) >> 32;
+
+               db -= change;
+       }
+
+       return value;
+}
+
+VISIBLE_IF_IWLWIFI_KUNIT s8
+iwl_mvm_average_dbm_values(const struct iwl_umac_scan_channel_survey_notif *notif)
+{
+       s8 average_magnitude;
+       u32 average_factor;
+       s8 sum_magnitude = -128;
+       u32 sum_factor = 0;
+       int i, count = 0;
+
+       /*
+        * To properly average the decibel values (signal values given in dBm)
+        * we need to do the math in linear space.  Doing a linear average of
+        * dB (dBm) values is a bit annoying though due to the large range of
+        * at least -10 to -110 dBm that will not fit into a 32 bit integer.
+        *
+        * A 64 bit integer should be sufficient, but then we still have the
+        * problem that there are no directly usable utility functions
+        * available.
+        *
+        * So, lets not deal with that and instead do much of the calculation
+        * with a 16.16 fixed point integer along with a base in dBm. 16.16 bit
+        * gives us plenty of head-room for adding up a few values and even
+        * doing some math on it. And the tail should be accurate enough too
+        * (1/2^16 is somewhere around -48 dB, so effectively zero).
+        *
+        * i.e. the real value of sum is:
+        *      sum = sum_factor / 2^16 * 10^(sum_magnitude / 10) mW
+        *
+        * However, that does mean we need to be able to bring two values to
+        * a common base, so we need a helper for that.
+        *
+        * Note that this function takes an input with unsigned negative dBm
+        * values but returns a signed dBm (i.e. a negative value).
+        */
+
+       for (i = 0; i < ARRAY_SIZE(notif->noise); i++) {
+               s8 val_magnitude;
+               u32 val_factor;
+
+               if (notif->noise[i] == 0xff)
+                       continue;
+
+               val_factor = 0x10000;
+               val_magnitude = -notif->noise[i];
+
+               if (val_magnitude <= sum_magnitude) {
+                       u8 div_db = sum_magnitude - val_magnitude;
+
+                       val_factor = iwl_mvm_div_by_db(val_factor, div_db);
+                       val_magnitude = sum_magnitude;
+               } else {
+                       u8 div_db = val_magnitude - sum_magnitude;
+
+                       sum_factor = iwl_mvm_div_by_db(sum_factor, div_db);
+                       sum_magnitude = val_magnitude;
+               }
+
+               sum_factor += val_factor;
+               count++;
+       }
+
+       /* No valid noise measurement, return a very high noise level */
+       if (count == 0)
+               return 0;
+
+       average_magnitude = sum_magnitude;
+       average_factor = sum_factor / count;
+
+       /*
+        * average_factor will be a number smaller than 1.0 (0x10000) at this
+        * point. What we need to do now is to adjust average_magnitude so that
+        * average_factor is between -0.5 dB and 0.5 dB.
+        *
+        * Just do -1 dB steps and find the point where
+        *   -0.5 dB * -i dB = 0x10000 * 10^(-0.5/10) / i dB
+        *                   = div_by_db(0xe429, i)
+        * is smaller than average_factor.
+        */
+       for (i = 0; average_factor < iwl_mvm_div_by_db(0xe429, i); i++) {
+               /* nothing */
+       }
+
+       return average_magnitude - i;
+}
+EXPORT_SYMBOL_IF_IWLWIFI_KUNIT(iwl_mvm_average_dbm_values);
+
+void iwl_mvm_rx_channel_survey_notif(struct iwl_mvm *mvm,
+                                    struct iwl_rx_cmd_buffer *rxb)
+{
+       struct iwl_rx_packet *pkt = rxb_addr(rxb);
+       const struct iwl_umac_scan_channel_survey_notif *notif =
+               (void *)pkt->data;
+       struct iwl_mvm_acs_survey_channel *info;
+       enum nl80211_band band;
+       int chan_idx;
+
+       lockdep_assert_held(&mvm->mutex);
+
+       if (!mvm->acs_survey) {
+               size_t n_channels = 0;
+
+               for (band = 0; band < NUM_NL80211_BANDS; band++) {
+                       if (!mvm->hw->wiphy->bands[band])
+                               continue;
+
+                       n_channels += mvm->hw->wiphy->bands[band]->n_channels;
+               }
+
+               mvm->acs_survey = kzalloc(struct_size(mvm->acs_survey,
+                                                     channels, n_channels),
+                                         GFP_KERNEL);
+
+               if (!mvm->acs_survey)
+                       return;
+
+               mvm->acs_survey->n_channels = n_channels;
+               n_channels = 0;
+               for (band = 0; band < NUM_NL80211_BANDS; band++) {
+                       if (!mvm->hw->wiphy->bands[band])
+                               continue;
+
+                       mvm->acs_survey->bands[band] =
+                               &mvm->acs_survey->channels[n_channels];
+                       n_channels += mvm->hw->wiphy->bands[band]->n_channels;
+               }
+       }
+
+       band = iwl_mvm_nl80211_band_from_phy(le32_to_cpu(notif->band));
+       chan_idx = iwl_mvm_chanidx_from_phy(mvm, band,
+                                           le32_to_cpu(notif->channel));
+       if (WARN_ON_ONCE(chan_idx < 0))
+               return;
+
+       IWL_DEBUG_SCAN(mvm, "channel survey received for freq %d\n",
+                      mvm->hw->wiphy->bands[band]->channels[chan_idx].center_freq);
+
+       info = &mvm->acs_survey->bands[band][chan_idx];
+
+       /* Times are all in ms */
+       info->time = le32_to_cpu(notif->active_time);
+       info->time_busy = le32_to_cpu(notif->busy_time);
+       info->time_rx = le32_to_cpu(notif->rx_time);
+       info->time_tx = le32_to_cpu(notif->tx_time);
+       info->noise = iwl_mvm_average_dbm_values(notif);
+}
 
-iwlmvm-tests-y += module.o links.o
+iwlmvm-tests-y += module.o links.o scan.o
 
 obj-$(CONFIG_IWLWIFI_KUNIT_TESTS) += iwlmvm-tests.o
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for channel helper functions
+ *
+ * Copyright (C) 2024 Intel Corporation
+ */
+#include <net/mac80211.h>
+#include "../mvm.h"
+#include <kunit/test.h>
+
+MODULE_IMPORT_NS(EXPORTED_FOR_KUNIT_TESTING);
+
+static const struct acs_average_db_case {
+       const char *desc;
+       u8 neg_dbm[22];
+       s8 result;
+} acs_average_db_cases[] = {
+       {
+               .desc = "Smallest possible value, all filled",
+               .neg_dbm = {
+                       128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
+                       128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
+                       128, 128
+               },
+               .result = -128,
+       },
+       {
+               .desc = "Biggest possible value, all filled",
+               .neg_dbm = {
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                       0, 0,
+               },
+               .result = 0,
+       },
+       {
+               .desc = "Smallest possible value, partial filled",
+               .neg_dbm = {
+                       128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
+                       0xff, 0xff, 0xff, 0xff, 0xff,
+                       0xff, 0xff, 0xff, 0xff, 0xff,
+                       0xff, 0xff,
+               },
+               .result = -128,
+       },
+       {
+               .desc = "Biggest possible value, partial filled",
+               .neg_dbm = {
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                       0xff, 0xff, 0xff, 0xff, 0xff,
+                       0xff, 0xff, 0xff, 0xff, 0xff,
+                       0xff, 0xff,
+               },
+               .result = 0,
+       },
+       {
+               .desc = "Adding -80dBm to -75dBm until it is still rounded to -79dBm",
+               .neg_dbm = {
+                       75, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+                       80, 80, 80, 80, 80, 80, 80, 0xff, 0xff, 0xff,
+                       0xff, 0xff,
+               },
+               .result = -79,
+       },
+       {
+               .desc = "Adding -80dBm to -75dBm until it is just rounded to -80dBm",
+               .neg_dbm = {
+                       75, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+                       80, 80, 80, 80, 80, 80, 80, 80, 0xff, 0xff,
+                       0xff, 0xff,
+               },
+               .result = -80,
+       },
+};
+
+KUNIT_ARRAY_PARAM_DESC(acs_average_db, acs_average_db_cases, desc)
+
+static void test_acs_average_db(struct kunit *test)
+{
+       const struct acs_average_db_case *params = test->param_value;
+       struct iwl_umac_scan_channel_survey_notif notif;
+       int i;
+
+       /* Test the values in the given order */
+       for (i = 0; i < ARRAY_SIZE(params->neg_dbm); i++)
+               notif.noise[i] = params->neg_dbm[i];
+       KUNIT_ASSERT_EQ(test,
+                       iwl_mvm_average_dbm_values(¬if),
+                       params->result);
+
+       /* Test in reverse order */
+       for (i = 0; i < ARRAY_SIZE(params->neg_dbm); i++)
+               notif.noise[ARRAY_SIZE(params->neg_dbm) - i - 1] =
+                       params->neg_dbm[i];
+       KUNIT_ASSERT_EQ(test,
+                       iwl_mvm_average_dbm_values(¬if),
+                       params->result);
+}
+
+static struct kunit_case acs_average_db_case[] = {
+       KUNIT_CASE_PARAM(test_acs_average_db, acs_average_db_gen_params),
+       {}
+};
+
+static struct kunit_suite acs_average_db = {
+       .name = "iwlmvm-acs-average-db",
+       .test_cases = acs_average_db_case,
+};
+
+kunit_test_suite(acs_average_db);