diff --git a/drivers/net/wireless/celeno/cl8k/scan.c b/drivers/net/wireless/celeno/cl8k/scan.c
new file mode 100644
index 000000000000..10076d93620e
--- /dev/null
+++ b/drivers/net/wireless/celeno/cl8k/scan.c
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause
+/* Copyright(c) 2019-2022, Celeno Communications Ltd. */
+
+#include <linux/err.h>
+#include <linux/mutex.h>
+#include <linux/delay.h>
+#include <linux/slab.h>
+
+#include "channel.h"
+#include "chip.h"
+#include "calib.h"
+#include "debug.h"
+#include "rates.h"
+#include "vif.h"
+#include "hw.h"
+#include "scan.h"
+
+#define CL_MIN_SCAN_TIME_MS 50
+#define CL_MIN_WAIT_TIME_MS 20
+
+static const char SCANNER_KTHREAD_NAME[] = "cl_scanner_kthread";
+
+int cl_scan_channel_switch(struct cl_hw *cl_hw, u8 channel, u8 bw,
+			   bool allow_recalib)
+{
+	struct cl_vif *cl_vif = cl_vif_get_first(cl_hw);
+	struct cfg80211_chan_def *chandef = NULL;
+	struct cfg80211_chan_def local_chandef;
+	struct ieee80211_channel *chan = NULL;
+	u16 freq = ieee80211_channel_to_frequency(channel, cl_hw->nl_band);
+	int ret = 0;
+
+	if (!cl_vif) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	chandef = &cl_vif->vif->bss_conf.chandef;
+	local_chandef = *chandef;
+
+	chan = ieee80211_get_channel(cl_hw->hw->wiphy, freq);
+	if (!chan) {
+		cl_dbg_err(cl_hw, "Channel %u wasn't found!\n", channel);
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	local_chandef.chan = chan;
+	if (cl_chandef_calc(cl_hw, channel, bw,
+			    &local_chandef.width,
+			    &local_chandef.chan->center_freq,
+			    &local_chandef.center_freq1)) {
+		cl_dbg_err(cl_hw, "Failed to extract chandef data for ch:%d\n",
+			   channel);
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	*chandef = local_chandef;
+	cl_hw->hw->conf.chandef = local_chandef;
+
+	if (cl_hw->chip->conf->ce_calib_runtime_en && allow_recalib)
+		ret = cl_calib_runtime_and_switch_channel(cl_hw, channel, bw,
+							  freq,
+							  chandef->center_freq1);
+	else
+		ret = cl_msg_tx_set_channel(cl_hw, channel, bw, freq,
+					    chandef->center_freq1,
+					    CL_CALIB_PARAMS_DEFAULT_STRUCT);
+exit:
+	return ret;
+}
+
+static int cl_scan_channel(struct cl_chan_scanner *scanner, u8 ch_idx)
+{
+	u8 main_channel;
+	enum cl_channel_bw main_bw;
+	s32 res = 0;
+	bool is_off_channel;
+	u64 scan_time_jiffies;
+
+	/*
+	 * 1. Save current channel
+	 * 2. Disable tx
+	 * 3. jump to new channel
+	 * 4. Enable promiscious
+	 * 5. Enable BSS collection
+	 * 6. Reset stats counters
+	 * 7. Sleep for scan_time
+	 * 8. Calculate stats
+	 * 9. Disable promiscious
+	 * 10. Disable BSS collection
+	 * 11. Switch to current channel
+	 * 12. Enable tx
+	 **/
+
+	cl_dbg_trace(scanner->cl_hw, "Starting scan on channel %u, scan time %u(ms)\n",
+		     scanner->channels[ch_idx].channel, scanner->scan_time);
+
+	/* Save current channel */
+	res = mutex_lock_interruptible(&scanner->cl_hw->set_channel_mutex);
+	if (res != 0)
+		return res;
+	main_channel = scanner->cl_hw->channel;
+	main_bw = scanner->cl_hw->bw;
+	mutex_unlock(&scanner->cl_hw->set_channel_mutex);
+
+	cl_dbg_trace(scanner->cl_hw, "Main channel is %u with bw %u\n",
+		     main_channel, main_bw);
+
+	is_off_channel = (scanner->channels[ch_idx].channel != main_channel) ||
+			 (scanner->channels[ch_idx].scan_bw != main_bw);
+
+	/* Jump to new channel */
+	if (is_off_channel) {
+		/* Disable tx */
+		cl_tx_en(scanner->cl_hw, CL_TX_EN_SCAN, false);
+
+		res = cl_scan_channel_switch(scanner->cl_hw,
+					     scanner->channels[ch_idx].channel,
+					     scanner->channels[ch_idx].scan_bw,
+					     false);
+		if (res) {
+			cl_dbg_err(scanner->cl_hw,
+				   "Channel switch failed: ch - %u, bw - %u, err - %d\n",
+				   scanner->channels[ch_idx].channel,
+				   scanner->channels[ch_idx].scan_bw, res);
+			goto enable_tx;
+		}
+	} else {
+		cl_dbg_trace(scanner->cl_hw, "Scan on main channel %u\n", main_channel);
+	}
+
+	/* Enable promiscious mode */
+	cl_rx_filter_set_promiscuous(scanner->cl_hw);
+
+	/* Reset channel stats */
+	cl_get_initial_channel_stats(scanner->cl_hw, &scanner->channels[ch_idx]);
+
+	/* Sleep for scan time */
+	scan_time_jiffies = msecs_to_jiffies(scanner->scan_time);
+	res = wait_for_completion_interruptible_timeout(&scanner->abort_completion,
+							scan_time_jiffies);
+	if (res > 0) {
+		cl_dbg_err(scanner->cl_hw, "Scan on channel %u, bw %u, idx %u was aborted\n",
+			   scanner->channels[ch_idx].channel,
+			   scanner->channels[ch_idx].scan_bw, ch_idx);
+		res = 0;
+	}
+
+	/* Calculate stats */
+	cl_get_final_channel_stats(scanner->cl_hw, &scanner->channels[ch_idx]);
+
+	/* Disable promiscious */
+	cl_rx_filter_restore_flags(scanner->cl_hw);
+
+	if (is_off_channel) {
+		res = cl_scan_channel_switch(scanner->cl_hw, main_channel, main_bw, false);
+		if (res)
+			cl_dbg_err(scanner->cl_hw,
+				   "Switching to main ch %u, bw %u failed, err - %d\n",
+				   main_channel, main_bw, res);
+enable_tx:
+		/* Enable tx */
+		cl_tx_en(scanner->cl_hw, CL_TX_EN_SCAN, true);
+	}
+
+	cl_dbg_trace(scanner->cl_hw, "Scan on channel %u finished, actual scan_time is %u ms\n",
+		     scanner->channels[ch_idx].channel, scanner->channels[ch_idx].scan_time_ms);
+
+	return res;
+}
+
+static s32 cl_run_off_channel_scan(struct cl_chan_scanner *scanner)
+{
+	u8 i = 0, scanned_channels = 0;
+	s32 ret = 0;
+
+	for (i = 0; i < scanner->channels_num && !scanner->scan_aborted; ++i) {
+		if (!scanner->channels[i].scan_enabled)
+			continue;
+
+		scanner->curr_ch_idx = i;
+		ret = cl_scan_channel(scanner, i);
+		if (ret)
+			cl_dbg_err(scanner->cl_hw, "scan failed, err - %d, channel - %u\n",
+				   ret, scanner->channels[i].channel);
+
+		if (scanner->scan_aborted)
+			break;
+
+		cl_dbg_trace(scanner->cl_hw, "Scan on chan %u finished, waiting for time %u\n",
+			     scanner->channels[i].channel, scanner->wait_time);
+
+		++scanned_channels;
+		if (scanned_channels != scanner->scan_channels_num) {
+			u64 wait_time_jiffies;
+
+			wait_time_jiffies = msecs_to_jiffies(scanner->wait_time);
+			ret = wait_for_completion_interruptible_timeout(&scanner->abort_completion,
+									wait_time_jiffies);
+			if (ret > 0) {
+				cl_dbg_err(scanner->cl_hw, "Off-channel scan was aborted\n");
+				ret = 0;
+			}
+		}
+	}
+
+	if (scanner->completion_cb)
+		scanner->completion_cb(scanner->cl_hw, scanner->completion_arg);
+
+	cl_dbg_info(scanner->cl_hw, "Off-channel scan on %u channels finished\n",
+		    scanner->scan_channels_num);
+
+	return ret;
+}
+
+static s32 cl_off_channel_scan_thread_fn(void *args)
+{
+	struct cl_chan_scanner *scanner = args;
+
+	while (!kthread_should_stop()) {
+		if (atomic_read(&scanner->scan_thread_busy)) {
+			cl_run_off_channel_scan(scanner);
+			atomic_set(&scanner->scan_thread_busy, 0);
+			wake_up_interruptible(&scanner->wq);
+		}
+
+		set_current_state(TASK_INTERRUPTIBLE);
+		schedule();
+	}
+
+	return 0;
+}
+
+static bool cl_is_scan_available(struct cl_chan_scanner *scanner)
+{
+	if (atomic_cmpxchg(&scanner->scan_thread_busy, 0, 1) == 1) {
+		cl_dbg_warn(scanner->cl_hw, "Off-channel scan is already in progress\n");
+		return false;
+	}
+
+	return true;
+}
+
+static enum cl_channel_bw cl_scanner_fix_input_bw(struct cl_chan_scanner *scanner, u8 bw)
+{
+	return (bw >= CHNL_BW_MAX) ? scanner->cl_hw->bw : bw;
+}
+
+static void cl_scanner_disable_everywhere(struct cl_chan_scanner *scanner)
+{
+	u8 j;
+
+	for (j = 0; j < scanner->channels_num; ++j)
+		scanner->channels[j].scan_enabled = false;
+}
+
+s32 cl_trigger_off_channel_scan(struct cl_chan_scanner *scanner, u32 scan_time, u32 wait_time,
+				const u8 *channels, enum cl_channel_bw scan_bw, u8 channels_num,
+				void (*completion_cb)(struct cl_hw *cl_hw, void *arg),
+				void *completion_arg)
+{
+	u8 i, j;
+
+	if (!channels || scan_bw > CHNL_BW_MAX)
+		return -EINVAL;
+
+	if (!scanner->scans_enabled)
+		return 0;
+
+	if (channels_num > scanner->channels_num) {
+		cl_dbg_err(scanner->cl_hw, "channels num %u is invalid, max is %u\n",
+			   channels_num, scanner->channels_num);
+		return -ERANGE;
+	}
+
+	if (!cl_is_scan_available(scanner))
+		return -EBUSY;
+
+	scanner->completion_arg = completion_arg;
+	scanner->completion_cb = completion_cb;
+	scanner->scan_time = max_t(u32, scan_time, CL_MIN_SCAN_TIME_MS);
+	scanner->wait_time = max_t(u32, wait_time, CL_MIN_WAIT_TIME_MS);
+	scanner->scan_bw = cl_scanner_fix_input_bw(scanner, scan_bw);
+	scanner->scan_aborted = false;
+
+	cl_scanner_disable_everywhere(scanner);
+
+	scanner->scan_channels_num = 0;
+	for (j = 0; j < scanner->channels_num; ++j) {
+		for (i = 0; i < channels_num; ++i) {
+			if (channels[i] != scanner->channels[j].channel)
+				continue;
+
+			if (!cl_chan_info_get(scanner->cl_hw, scanner->channels[j].channel,
+					      scanner->scan_bw)) {
+				cl_dbg_warn(scanner->cl_hw, "channel %u with bw %u is disabled\n",
+					    scanner->channels[j].channel, scanner->scan_bw);
+				continue;
+			}
+
+			scanner->channels[j].scan_enabled = true;
+			++scanner->scan_channels_num;
+		}
+	}
+
+	reinit_completion(&scanner->abort_completion);
+
+	wake_up_process(scanner->scan_thread);
+
+	return 0;
+}
+
+void cl_abort_scan(struct cl_chan_scanner *scanner)
+{
+	scanner->scan_aborted = true;
+	complete(&scanner->abort_completion);
+	cl_dbg_info(scanner->cl_hw, "Off-channel scan was aborted\n");
+}
+
+bool cl_is_scan_in_progress(const struct cl_chan_scanner *scanner)
+{
+	return atomic_read(&scanner->scan_thread_busy);
+}
+
+int cl_scanner_init(struct cl_hw *cl_hw)
+{
+	u8 i, j;
+	s32 ret = 0;
+	u32 channels_num;
+	struct cl_chan_scanner *scanner;
+
+	cl_hw->scanner = vzalloc(sizeof(*cl_hw->scanner));
+	if (!cl_hw->scanner)
+		return -ENOMEM;
+
+	scanner = cl_hw->scanner;
+	init_completion(&scanner->abort_completion);
+
+	scanner->cl_hw = cl_hw;
+	scanner->scans_enabled = true;
+
+	channels_num = cl_channel_num(scanner->cl_hw);
+	for (i = 0, j = 0; i < channels_num; ++i) {
+		u32 freq;
+
+		freq = cl_channel_idx_to_freq(cl_hw, i);
+		if (!ieee80211_get_channel(cl_hw->hw->wiphy, freq))
+			continue;
+
+		ret = cl_init_channel_stats(scanner->cl_hw, &scanner->channels[j], freq);
+		if (ret)
+			return ret;
+
+		cl_dbg_trace(scanner->cl_hw, "Stats for channel %u at index %u initialized\n",
+			     scanner->channels[j].channel, j);
+		++j;
+	}
+
+	scanner->channels_num = j;
+
+	atomic_set(&scanner->scan_thread_busy, 0);
+	init_waitqueue_head(&scanner->wq);
+
+	scanner->scan_thread = kthread_run(cl_off_channel_scan_thread_fn,
+					   scanner, SCANNER_KTHREAD_NAME);
+	if (IS_ERR(scanner->scan_thread)) {
+		cl_dbg_err(scanner->cl_hw, "unable to create kthread %s, err - %ld\n",
+			   SCANNER_KTHREAD_NAME, PTR_ERR(scanner->scan_thread));
+		return PTR_ERR(scanner->scan_thread);
+	}
+	cl_dbg_trace(scanner->cl_hw, "%s kthread was created, pid - %u\n",
+		     SCANNER_KTHREAD_NAME, scanner->scan_thread->pid);
+
+	return ret;
+}
+
+void cl_scanner_deinit(struct cl_hw *cl_hw)
+{
+	struct cl_chan_scanner *scanner = cl_hw->scanner;
+
+	if (!scanner->scans_enabled)
+		goto out;
+
+	if (scanner->scan_thread)
+		kthread_stop(scanner->scan_thread);
+
+ out:
+	vfree(cl_hw->scanner);
+	cl_hw->scanner = NULL;
+}
